{
  "components": {
    "schemas": {
      "AcceptChannelInviteRequest": {
        "description": "POST /testers/invites/accept body.",
        "properties": {
          "invite_token": {
            "format": "password",
            "title": "Invite Token",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "invite_token"
        ],
        "title": "AcceptChannelInviteRequest",
        "type": "object"
      },
      "AcceptChannelInviteResponse": {
        "description": "Successful accept response.",
        "properties": {
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "project_id",
          "channel_id"
        ],
        "title": "AcceptChannelInviteResponse",
        "type": "object"
      },
      "AcceptInvite": {
        "description": "Body of POST /invites/{kind}/accept - kind is path-level.\n\nThe bearer token authenticates the developer; the body carries\nthe invite token so the same request that proves identity also\nproves possession of the invite link.",
        "properties": {
          "invite_token": {
            "format": "password",
            "title": "Invite Token",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "invite_token"
        ],
        "title": "AcceptInvite",
        "type": "object"
      },
      "AccountDeletionCancelRequest": {
        "description": "Request body for ``POST /developers/cancel_deletion``.\n\nThe ``token`` resolves to a developer_id at consume time via the\nDELETE-RETURNING in ``consume_and_reactivate``; no developer_id needs to\nride in the body. The route is public (no JWT): the owner is locked out of\nauth while pending deletion, so the emailed single-use token is their only\ncredential to reactivate.",
        "properties": {
          "token": {
            "format": "password",
            "title": "Token",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "token"
        ],
        "title": "AccountDeletionCancelRequest",
        "type": "object"
      },
      "AddTesterRequest": {
        "description": "Body of POST /.../testers \u2014 admin adds a tester by email.\n\nUsed at both org and project tiers. If a developer with this\nemail already exists, the membership row lands immediately. If\nnot, the email goes into the matching pending_*_tester_grants\ntable and auto-attaches on email-verification later.",
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "AddTesterRequest",
        "type": "object"
      },
      "AuthenticationBeginResponse": {
        "description": "The server's reply to \"begin a passwordless passkey login\".\n\n``options`` is the WebAuthn ``PublicKeyCredentialRequestOptions`` JSON for\n``navigator.credentials.get()``. ``challenge_id`` is the handle for the\nanonymous server-stored challenge, echoed back on finish.",
        "properties": {
          "challenge_id": {
            "format": "uuid",
            "title": "Challenge Id",
            "type": "string"
          },
          "options": {
            "additionalProperties": true,
            "title": "Options",
            "type": "object"
          }
        },
        "required": [
          "challenge_id",
          "options"
        ],
        "title": "AuthenticationBeginResponse",
        "type": "object"
      },
      "AuthenticationFinishRequest": {
        "description": "The browser's ``navigator.credentials.get()`` assertion, plus context.",
        "properties": {
          "challenge_id": {
            "format": "uuid",
            "title": "Challenge Id",
            "type": "string"
          },
          "credential": {
            "additionalProperties": true,
            "title": "Credential",
            "type": "object"
          }
        },
        "required": [
          "challenge_id",
          "credential"
        ],
        "title": "AuthenticationFinishRequest",
        "type": "object"
      },
      "BYOSAValidationReport": {
        "description": "The result of a storage validation or diagnose run. ``failure_stage`` is\n``None`` on success.\n\n``failure_stage`` names the check that failed: ``token_exchange``,\n``container_check``, ``container_not_found``, ``put_marker``,\n``delete_marker``, ``list_container`` or ``soft_delete_residue``.\n\nOn failure the report carries structured remediation:\n\n* ``responsibility`` \u2014 ``\"customer\"`` (something on your side to fix) or\n  ``\"platform\"`` (raise a support ticket).\n* ``summary`` \u2014 a one-line description of what went wrong.\n* ``remediation`` \u2014 the concrete next steps to take.\n* ``docs_url`` \u2014 a link to the matching section of the setup guide.\n* ``failure_detail`` \u2014 the storage provider's own error code and message,\n  quoted so you can correlate it against your cloud logs.\n\nOn success every field except ``success`` is ``None`` or empty.",
        "properties": {
          "docs_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Docs Url"
          },
          "failure_detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Failure Detail"
          },
          "failure_stage": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Failure Stage"
          },
          "remediation": {
            "items": {
              "type": "string"
            },
            "title": "Remediation",
            "type": "array"
          },
          "responsibility": {
            "anyOf": [
              {
                "enum": [
                  "customer",
                  "platform"
                ],
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Responsibility"
          },
          "success": {
            "title": "Success",
            "type": "boolean"
          },
          "summary": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Summary"
          }
        },
        "required": [
          "success"
        ],
        "title": "BYOSAValidationReport",
        "type": "object"
      },
      "Body_set_developer_avatar_api_v1_developers__developer_id__avatar_post": {
        "properties": {
          "file": {
            "contentMediaType": "application/octet-stream",
            "title": "File",
            "type": "string"
          }
        },
        "required": [
          "file"
        ],
        "title": "Body_set_developer_avatar_api_v1_developers__developer_id__avatar_post",
        "type": "object"
      },
      "Body_set_organization_icon_api_v1_organizations__organization_id__icon_post": {
        "properties": {
          "file": {
            "contentMediaType": "application/octet-stream",
            "title": "File",
            "type": "string"
          }
        },
        "required": [
          "file"
        ],
        "title": "Body_set_organization_icon_api_v1_organizations__organization_id__icon_post",
        "type": "object"
      },
      "Body_set_project_icon_api_v1_organizations__organization_id__projects__project_id__icon_post": {
        "properties": {
          "file": {
            "contentMediaType": "application/octet-stream",
            "title": "File",
            "type": "string"
          }
        },
        "required": [
          "file"
        ],
        "title": "Body_set_project_icon_api_v1_organizations__organization_id__projects__project_id__icon_post",
        "type": "object"
      },
      "Body_set_project_icon_api_v1_projects__project_id__icon_post": {
        "properties": {
          "file": {
            "contentMediaType": "application/octet-stream",
            "title": "File",
            "type": "string"
          }
        },
        "required": [
          "file"
        ],
        "title": "Body_set_project_icon_api_v1_projects__project_id__icon_post",
        "type": "object"
      },
      "BuildDownloadResponse": {
        "description": "Signed download URL handed back to a build-download caller.\n\nThe ``/builds/{id}/download`` route returns this JSON rather than a 302\nredirect: the web portal needs the URL as data (to fetch it via XHR, show\nprogress, or open it in a new tab) instead of having the browser follow an\nopaque redirect. Programmatic clients read ``download_url`` and issue the\nGET themselves. The URL is a short-lived signed link bound to the calling\nsubject.",
        "properties": {
          "download_url": {
            "title": "Download Url",
            "type": "string"
          }
        },
        "required": [
          "download_url"
        ],
        "title": "BuildDownloadResponse",
        "type": "object"
      },
      "BuildPage": {
        "description": "Cursor-pagination page of builds. ``items`` is the page, each entry\ncarrying the server-derived ``icon_url``. ``next_cursor`` is the opaque\ntoken to pass back for the next page; a null ``next_cursor`` means this\nis the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/BuildResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "BuildPage",
        "type": "object"
      },
      "BuildResponse": {
        "description": "Wire-format projection of a build.",
        "properties": {
          "build_number": {
            "title": "Build Number",
            "type": "integer"
          },
          "bundle_identifier": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bundle Identifier"
          },
          "has_icon": {
            "default": false,
            "title": "Has Icon",
            "type": "boolean"
          },
          "icon_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Icon Url"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "min_os_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Min Os Version"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "original_filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Original Filename"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "release_notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "sha256": {
            "title": "Sha256",
            "type": "string"
          },
          "uploaded_at": {
            "format": "date-time",
            "title": "Uploaded At",
            "type": "string"
          },
          "uploaded_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Uploaded By Developer Id"
          },
          "version_name": {
            "title": "Version Name",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "platform",
          "version_name",
          "build_number",
          "sha256",
          "uploaded_at"
        ],
        "title": "BuildResponse",
        "type": "object"
      },
      "BuildStatusFilter": {
        "description": "Revocation-status filter for the build-list endpoint.\n\n``active`` returns only un-revoked builds, ``revoked`` only revoked ones,\nand ``all`` both. When the query omits ``status`` the endpoint falls back to\nthe legacy ``include_revoked`` flag (``False`` -> active, ``True`` -> all),\nso pre-existing callers keep working unchanged.\n\nString values are the wire form (``?status=active``); they are not persisted\nanywhere, so they are free to change, unlike ``Platform``.",
        "enum": [
          "active",
          "revoked",
          "all"
        ],
        "title": "BuildStatusFilter",
        "type": "string"
      },
      "CardCaptureResponse": {
        "description": "The minted Paddle checkout for adding a card on file.\n\nPaddle Billing vaults a card only via a subscription-tied checkout, so this is\neither a subscription-create checkout (recurring flat-fee Price, for an org with\nno subscription yet) or Paddle's zero-value update-payment-method transaction\n(for an already-subscribed org changing its card). ``transaction_id`` is what the\nfrontend opens with Paddle.js; ``checkout_url`` is the hosted-checkout link when\nPaddle exposes one (else null). ``card_on_file`` flips true only later, when\nPaddle's verified ``transaction.completed`` webhook lands.",
        "properties": {
          "checkout_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Checkout Url"
          },
          "transaction_id": {
            "title": "Transaction Id",
            "type": "string"
          }
        },
        "required": [
          "transaction_id",
          "checkout_url"
        ],
        "title": "CardCaptureResponse",
        "type": "object"
      },
      "ChangeEmailRequest": {
        "description": "Request body for ``POST /developers/{id}/change_email``.\n\n``new_email`` is the address to park as ``pending_email``. ``current_password``\nre-authenticates the live session. ``mfa_code`` is a TOTP or recovery code,\nrequired only when the developer has a confirmed TOTP factor (the service\nenforces this \u2014 an absent code for an MFA-enrolled developer is rejected).\n\nFor a passkey-only developer (no confirmed TOTP but at least one passkey), a\nWebAuthn step-up is required instead: the client first calls the\nstep-up begin endpoint, signs the returned challenge, and echoes back\n``passkey_challenge_id`` plus the assertion in ``passkey_assertion``. The\nservice verifies the assertion is bound to this developer before parking the\nchange.",
        "properties": {
          "current_password": {
            "format": "password",
            "title": "Current Password",
            "type": "string",
            "writeOnly": true
          },
          "mfa_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mfa Code"
          },
          "new_email": {
            "title": "New Email",
            "type": "string"
          },
          "passkey_assertion": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Passkey Assertion"
          },
          "passkey_challenge_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Passkey Challenge Id"
          }
        },
        "required": [
          "new_email",
          "current_password"
        ],
        "title": "ChangeEmailRequest",
        "type": "object"
      },
      "ChangeOrganizationMemberRole": {
        "description": "Body of PATCH /organizations/{id}/members/{developer_id}.",
        "properties": {
          "role": {
            "$ref": "#/components/schemas/OrganizationMemberRole"
          }
        },
        "required": [
          "role"
        ],
        "title": "ChangeOrganizationMemberRole",
        "type": "object"
      },
      "ChangeProjectMemberRole": {
        "description": "Body of PATCH /projects/{id}/members/{developer_id}.",
        "properties": {
          "role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          }
        },
        "required": [
          "role"
        ],
        "title": "ChangeProjectMemberRole",
        "type": "object"
      },
      "Channel": {
        "description": "A named distribution stream inside a project, such as a beta or a\nproduction channel.\n\nA channel holds a sequence of releases and serves one current release per\nplatform. Testers follow a channel rather than an individual build, so\npublishing a release reaches everyone the channel is shared with. A\n``public`` channel serves its current release to anyone with the link,\nwithout signing in.",
        "properties": {
          "auto_distribute": {
            "default": false,
            "title": "Auto Distribute",
            "type": "boolean"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "notify_on_release": {
            "default": true,
            "title": "Notify On Release",
            "type": "boolean"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "public": {
            "default": false,
            "title": "Public",
            "type": "boolean"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "name",
          "created_at"
        ],
        "title": "Channel",
        "type": "object"
      },
      "ChannelAnalytics": {
        "description": "Aggregate install/download counters for a whole channel.\n\nEvery download is stamped at download time with the channel it was served\nthrough, so these counters aggregate exactly the downloads that channel\nserved \u2014 a build released on several channels does not have its downloads\ncounted against all of them.\n\n``unique_testers`` counts distinct downloading developers; anonymised\ndownloads (the developer has since been deleted, or the download came through\na public link) still count toward ``total_downloads`` but not toward\n``unique_testers``. ``distinct_builds`` is how many distinct builds have been\ndownloaded through the channel. Downloads made outside any channel \u2014 a\ndeveloper fetching their own build directly, and history recorded before\nchannel stamping began \u2014 are excluded from every channel's totals.",
        "properties": {
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "distinct_builds": {
            "title": "Distinct Builds",
            "type": "integer"
          },
          "first_downloaded_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "First Downloaded At"
          },
          "last_downloaded_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Downloaded At"
          },
          "total_downloads": {
            "title": "Total Downloads",
            "type": "integer"
          },
          "unique_testers": {
            "title": "Unique Testers",
            "type": "integer"
          }
        },
        "required": [
          "channel_id",
          "distinct_builds",
          "total_downloads",
          "unique_testers",
          "first_downloaded_at",
          "last_downloaded_at"
        ],
        "title": "ChannelAnalytics",
        "type": "object"
      },
      "ChannelInvitePage": {
        "description": "Cursor-paginated page of pending channel tester invites.\n\n``items`` are ``ChannelTesterInvite`` with ``invite_token`` stripped (the\nlisting never carries the raw token). ``next_cursor`` is the opaque token to\npass back as ``cursor=...``; null means this was the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ChannelTesterInvite"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "ChannelInvitePage",
        "type": "object"
      },
      "ChannelInvitePreview": {
        "description": "GET /testers/invites/{token}/preview \u2014 interstitial info, no auth.",
        "properties": {
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "channel_name": {
            "title": "Channel Name",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "organization_name": {
            "title": "Organization Name",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "project_name": {
            "title": "Project Name",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "organization_name",
          "project_id",
          "project_name",
          "channel_id",
          "channel_name",
          "expires_at"
        ],
        "title": "ChannelInvitePreview",
        "type": "object"
      },
      "ChannelNotificationPreference": {
        "description": "Body/response of the tester per-channel release-notification toggle.\n\n``muted == True`` means the caller has opted out of \"new release available\"\nemails for this specific channel. Absence of an opt-out (the default) is\n``muted == False``.",
        "properties": {
          "muted": {
            "title": "Muted",
            "type": "boolean"
          }
        },
        "required": [
          "muted"
        ],
        "title": "ChannelNotificationPreference",
        "type": "object"
      },
      "ChannelPage": {
        "description": "Cursor-paginated page of channels. ``next_cursor`` is the opaque\ntoken to pass back as ``cursor=...``; null means this was the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/Channel"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "ChannelPage",
        "type": "object"
      },
      "ChannelTesterInvite": {
        "description": "A pending channel tester invite.\n\nThe raw ``invite_token`` is on the wire ONLY for the create\nresponse \u2014 the admin needs it once to share the link. GET\nlistings strip it; only the hash lives in storage.",
        "properties": {
          "accepted_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accepted At"
          },
          "accepted_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accepted By Developer Id"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Email"
          },
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "invite_token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invite Token"
          },
          "invited_at": {
            "format": "date-time",
            "title": "Invited At",
            "type": "string"
          },
          "invited_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invited By Developer Id"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "channel_id",
          "invited_at",
          "expires_at"
        ],
        "title": "ChannelTesterInvite",
        "type": "object"
      },
      "CreateTesterGroupRequest": {
        "description": "Body of POST /organizations/{id}/tester-groups.",
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          },
          "notify_on_release": {
            "default": true,
            "title": "Notify On Release",
            "type": "boolean"
          }
        },
        "required": [
          "name"
        ],
        "title": "CreateTesterGroupRequest",
        "type": "object"
      },
      "CreditLedgerEntryResponse": {
        "description": "Wire shape for one prepaid credit-ledger row.\n\nAll money is integer cents. ``amount_cents`` is the positive magnitude;\n``kind`` (``topup``/``refund``) gives the sign. ``fee_cents`` is the withheld\nPaddle fee on a refund (null for a top-up). ``operator_name`` carries human\nattribution for an operator-initiated movement (null for a Paddle one).",
        "properties": {
          "amount_cents": {
            "title": "Amount Cents",
            "type": "integer"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "fee_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Fee Cents"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "kind": {
            "enum": [
              "topup",
              "refund"
            ],
            "title": "Kind",
            "type": "string"
          },
          "note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Note"
          },
          "operator_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Operator Name"
          },
          "source": {
            "enum": [
              "operator",
              "paddle"
            ],
            "title": "Source",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "amount_cents",
          "created_at",
          "source",
          "operator_name",
          "note",
          "kind",
          "fee_cents"
        ],
        "title": "CreditLedgerEntryResponse",
        "type": "object"
      },
      "CreditLedgerPage": {
        "description": "Paginated response for the credit-ledger listing.\n\nCursor pagination: ``items`` is the page (newest first), ``next_cursor`` is\nthe opaque token to pass back as ``cursor=...`` for the next page. A null\n``next_cursor`` means this was the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/CreditLedgerEntryResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "CreditLedgerPage",
        "type": "object"
      },
      "CustomerAuthTokenResponse": {
        "description": "A short-lived Paddle customer authentication token for saved-card checkout.\n\n``customer_auth_token`` (``pca_...``) is handed to Paddle.js as\n``customerAuthToken`` when opening a one-page checkout, which is what makes\nPaddle present the customer's saved payment methods (a card on file). The token\nis customer-scoped and expires quickly, so the frontend mints it per checkout\nand never stores it; ``expires_at`` is Paddle's RFC 3339 expiry so a stale token\nis not reused.\n\nBoth fields are null when the org has no Paddle customer / saved card on file. \"No saved card\" is a normal state (e.g. a brand-new org adding its first\ncard), not an error, so the endpoint succeeds with a null token and the frontend\nopens a plain checkout instead of treating a 404 as a failure.",
        "properties": {
          "customer_auth_token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Customer Auth Token"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          }
        },
        "required": [
          "customer_auth_token",
          "expires_at"
        ],
        "title": "CustomerAuthTokenResponse",
        "type": "object"
      },
      "CustomerSpendCapRequest": {
        "description": "Body for an org ADMIN setting their own monthly self-limit (customer spend cap).\n\n``customer_spend_cap_cents`` is the customer's own budget: ``None`` clears the\nself-limit (the org opts back out of self-budgeting). Unlike the operator route\nthis carries no attribution fields: the acting admin is the authenticated\ndeveloper, recorded as the audit ``editor_id`` directly. The value can only ever\n*lower* effective exposure below the operator ceiling (the pre-mint gate enforces\n``min(operator, customer)``), so a value above the ceiling is accepted but simply\nnever binds.",
        "properties": {
          "customer_spend_cap_cents": {
            "anyOf": [
              {
                "maximum": 1000000000.0,
                "minimum": 0.0,
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "New self-limit in cents; null = no self-limit.",
            "title": "Customer Spend Cap Cents"
          }
        },
        "title": "CustomerSpendCapRequest",
        "type": "object"
      },
      "Developer": {
        "description": "A developer account: the person who signs in, owns credentials, and holds\nmemberships in organizations.",
        "properties": {
          "avatar_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Avatar Url"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "email_verified": {
            "title": "Email Verified",
            "type": "boolean"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "max_organizations": {
            "title": "Max Organizations",
            "type": "integer"
          },
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "name",
          "email",
          "email_verified",
          "max_organizations"
        ],
        "title": "Developer",
        "type": "object"
      },
      "DeviceEnrollment": {
        "description": "The response to a mint-enrollment request.\n\n``enrollment_url`` is the raw REST Profile Service endpoint that serves the\n``.mobileconfig``; ``token`` is the same short-lived capability token that\nappears in that URL, exposed separately so a same-origin frontend can build\nits own tester-facing landing page around it without parsing the\ntoken back out of the URL. ``expires_in`` is the token's remaining lifetime\nin seconds.",
        "properties": {
          "enrollment_url": {
            "title": "Enrollment Url",
            "type": "string"
          },
          "expires_in": {
            "title": "Expires In",
            "type": "integer"
          },
          "token": {
            "title": "Token",
            "type": "string"
          }
        },
        "required": [
          "enrollment_url",
          "token",
          "expires_in"
        ],
        "title": "DeviceEnrollment",
        "type": "object"
      },
      "DevicePage": {
        "description": "Paginated response shape for the device-list endpoint.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/DeviceResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "DevicePage",
        "type": "object"
      },
      "DeviceRegistrationOutcome": {
        "description": "The result of trying to register one device with App Store Connect.\n\n``status`` is ``registered`` (newly added this sync), ``already_registered``\n(Apple already knew the UDID \u2014 a no-op), or ``failed`` (the per-device API\ncall errored; ``detail`` carries a short reason). A single device failing\ndoes not abort the others.",
        "properties": {
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Detail"
          },
          "device_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Device Name"
          },
          "status": {
            "enum": [
              "registered",
              "already_registered",
              "failed"
            ],
            "title": "Status",
            "type": "string"
          },
          "udid": {
            "title": "Udid",
            "type": "string"
          }
        },
        "required": [
          "udid",
          "status"
        ],
        "title": "DeviceRegistrationOutcome",
        "type": "object"
      },
      "DeviceRegistrationResult": {
        "description": "Summary of a device-registration sync against a project's ASC account.",
        "properties": {
          "already_registered": {
            "title": "Already Registered",
            "type": "integer"
          },
          "connection_id": {
            "format": "uuid",
            "title": "Connection Id",
            "type": "string"
          },
          "failed": {
            "title": "Failed",
            "type": "integer"
          },
          "newly_registered": {
            "title": "Newly Registered",
            "type": "integer"
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/DeviceRegistrationOutcome"
            },
            "title": "Outcomes",
            "type": "array"
          },
          "provider": {
            "title": "Provider",
            "type": "string"
          },
          "total_eligible": {
            "title": "Total Eligible",
            "type": "integer"
          }
        },
        "required": [
          "connection_id",
          "provider",
          "total_eligible",
          "newly_registered",
          "already_registered",
          "failed",
          "outcomes"
        ],
        "title": "DeviceRegistrationResult",
        "type": "object"
      },
      "DeviceResponse": {
        "description": "Wire-format projection of a registered device.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "device_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Device Name"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "model": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "os_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Os Version"
          },
          "platform": {
            "title": "Platform",
            "type": "string"
          },
          "registered_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Registered By Developer Id"
          },
          "udid": {
            "title": "Udid",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "udid",
          "platform",
          "created_at"
        ],
        "title": "DeviceResponse",
        "type": "object"
      },
      "Email": {
        "description": "Credentials for a developer sign-in request.\n\n``email`` and ``password`` are both required and length-bounded, and a value\ncarrying a control character such as NUL is rejected; surrounding whitespace is\nignored when the minimum length of ``email`` is measured. ``password`` is\nwrite-only and is never returned. ``captcha_key`` is optional and carries a\ncaptcha response token where sign-in is captcha-protected.\n\nInput that fails those checks is rejected with the canonical 400 /\ncode=\"validation_error\" response, whose message names the field at fault.\n\nThe address is deliberately not format-checked. Sign-in answers a malformed\naddress and an address no developer has with the same Invalid-credentials 401,\nso it cannot be used to discover whether an account exists.",
        "properties": {
          "captcha_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Captcha Key"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "password": {
            "format": "password",
            "title": "Password",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "email",
          "password"
        ],
        "title": "Email",
        "type": "object"
      },
      "ErrorResponse": {
        "description": "The error body returned by every failing request that references this component. `error` is a stable machine-readable code \u2014 branch on it rather than on the HTTP status, which several codes share. `details` is present on every response shaped by this component, and is an empty object when there is no structured detail.\n\nA 400 can be either this envelope (a business rule refused the request: `invalid_input`, `operation_not_allowed`, `quota_exceeded`) or the richer `ValidationErrorResponse` (the request failed validation), which is why operations that validate input declare both as an `anyOf`. A validation body satisfies both branches \u2014 the codes are what tell them apart \u2014 so read `error` first and branch on it.\n\nOne response shape is not this component at all: the SSO and storage-account read routes answer their *not configured yet* 404 with a bare `{error}` body carrying no `message` and no `details`, and declare that variant where it occurs.",
        "properties": {
          "details": {
            "additionalProperties": true,
            "description": "Structured detail for the error, or an empty object when there is none.",
            "type": "object"
          },
          "error": {
            "description": "Stable machine-readable code, for example `not_found` or `billing_suspended`.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable explanation, safe to surface in a user interface.",
            "type": "string"
          }
        },
        "required": [
          "error",
          "message",
          "details"
        ],
        "title": "ErrorResponse",
        "type": "object"
      },
      "GrantGroupRequest": {
        "description": "Body of POST /organizations/{id}/tester-groups/{group_id}/grants.\n\n``scope_type`` selects org / project / channel; the matching id must be set\n(and only that one). A malformed grant is rejected with a 4xx.",
        "properties": {
          "channel_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Channel Id"
          },
          "project_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project Id"
          },
          "scope_type": {
            "$ref": "#/components/schemas/TesterAccessTier"
          }
        },
        "required": [
          "scope_type"
        ],
        "title": "GrantGroupRequest",
        "type": "object"
      },
      "GrantableScopesResponse": {
        "description": "The PAT scopes the calling developer may mint in an organisation.\n\nThe read-side counterpart to the create-time privilege ceiling: the\ncreate route rejects an over-grant with 403, and this exposes the grantable\nset up front so the WebApp PAT-create UI can render only the scope\ncheckboxes the developer can actually pick, rather than letting them choose\none that would be rejected.\n\n``scopes`` is the raw ``PatScope`` bitmask, serialised as an int exactly\nlike every other PAT response's ``scopes`` field. ``grantable`` is the same\ninformation exploded into a per-scope boolean map (keyed by lower-cased\nscope name) so a client can bind one checkbox per scope without decoding\nthe bitfield itself. The composite ``ALL`` sentinel is deliberately absent\nfrom the map: it is not an individually-checkable scope, so a developer who\nmay mint everything simply has every entry set to ``True``.",
        "properties": {
          "grantable": {
            "additionalProperties": {
              "type": "boolean"
            },
            "title": "Grantable",
            "type": "object"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "scopes": {
            "$ref": "#/components/schemas/PatScope"
          }
        },
        "required": [
          "organization_id",
          "scopes",
          "grantable"
        ],
        "title": "GrantableScopesResponse",
        "type": "object"
      },
      "InstallMethod": {
        "description": "How a client should act on a release's ``install_url`` to install it.\n\nA single discriminator so every install surface (public channel page, tester\nrelease list, the WebApp) renders the right affordance without hardcoding\nper-platform logic:\n\n* ``OTA_MANIFEST`` \u2014 the platform installs over-the-air via an\n  ``itms-services`` manifest (iOS / visionOS). The client wraps the absolute\n  form of ``install_url`` in\n  ``itms-services://?action=download-manifest&url=...`` for tap-to-install\n  (public surface), or mints a tokenised OTA link via the dedicated\n  install-link endpoint (authenticated tester surface).\n* ``DIRECT_DOWNLOAD`` \u2014 the platform installs from the downloaded artifact\n  itself (Android .apk, and any non-OTA build). The client points the user\n  straight at ``install_url``.\n\nValues are part of the wire contract; add new members at the bottom and do\nnot change existing ones.",
        "enum": [
          "ota_manifest",
          "direct_download"
        ],
        "title": "InstallMethod",
        "type": "string"
      },
      "InvitePreview": {
        "description": "GET /invites/{token}/preview \u2014 interstitial info for an org/project invite.\n\nThe org/project analogue of ``ChannelInvitePreview``. It lets the WebApp\nshow the recipient *what* they've been invited to (org, optional project,\nrole) before they log in and accept, so an emailed link can be a plain GET\nthe user clicks rather than a \"POST this token\" instruction.\n\nNo auth: possession of the (unguessable, hashed-at-rest) token is the proof.\nEvery miss \u2014 unknown, expired, already-accepted, revoked \u2014 collapses to a\nsingle NOT_FOUND so a stolen token can't be used to probe org/project names.\n\n``project_id`` / ``project_name`` are populated only for project invites\n(``kind == \"project\"``); for an org invite they are ``None``. ``role`` is\nthe enum NAME (e.g. ``\"ADMIN\"``), matching the wire convention used\neverywhere else in this module.",
        "properties": {
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "kind": {
            "title": "Kind",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "organization_name": {
            "title": "Organization Name",
            "type": "string"
          },
          "project_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project Id"
          },
          "project_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project Name"
          },
          "role": {
            "title": "Role",
            "type": "string"
          }
        },
        "required": [
          "kind",
          "organization_id",
          "organization_name",
          "role",
          "expires_at"
        ],
        "title": "InvitePreview",
        "type": "object"
      },
      "MfaPasskeyVerifyRequest": {
        "description": "Body for completing a login MFA challenge with a passkey step-up.\n\n``mfa_token`` is the opaque challenge handed back by ``/login/email`` when the\naccount has a second factor; ``challenge_id`` + ``credential`` are the\nWebAuthn authentication ceremony begun at ``/login/mfa/passkey/begin`` and the\nbrowser's ``navigator.credentials.get()`` assertion. The server binds the\nasserted passkey to the challenge's developer, so a passkey belonging to a\ndifferent account cannot complete this login.",
        "properties": {
          "challenge_id": {
            "format": "uuid",
            "title": "Challenge Id",
            "type": "string"
          },
          "credential": {
            "additionalProperties": true,
            "title": "Credential",
            "type": "object"
          },
          "mfa_token": {
            "format": "password",
            "title": "Mfa Token",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "mfa_token",
          "challenge_id",
          "credential"
        ],
        "title": "MfaPasskeyVerifyRequest",
        "type": "object"
      },
      "MfaPasswordRequest": {
        "description": "Body for MFA operations gated by a password re-auth (disable, regenerate).\n\nRequiring the account password for these sensitive changes means a\nmomentarily-unlocked, already-logged-in session cannot silently strip a\ndeveloper's second factor or rotate their recovery codes.",
        "properties": {
          "password": {
            "format": "password",
            "title": "Password",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "password"
        ],
        "title": "MfaPasswordRequest",
        "type": "object"
      },
      "MfaStatusResponse": {
        "description": "A developer's current MFA state, for the account-settings screen.\n\n``enabled`` is True only once TOTP enrollment has been confirmed.\n``recovery_codes_remaining`` lets the UI nudge the user to regenerate when\nthey are running low.",
        "properties": {
          "enabled": {
            "title": "Enabled",
            "type": "boolean"
          },
          "recovery_codes_remaining": {
            "title": "Recovery Codes Remaining",
            "type": "integer"
          }
        },
        "required": [
          "enabled",
          "recovery_codes_remaining"
        ],
        "title": "MfaStatusResponse",
        "type": "object"
      },
      "MfaVerifyRequest": {
        "description": "Body for the second step of an MFA login.\n\n``mfa_token`` is the opaque challenge handed back by ``/login/email`` when the\naccount has MFA enabled; ``code`` is the TOTP code or a recovery code.",
        "properties": {
          "code": {
            "title": "Code",
            "type": "string"
          },
          "mfa_token": {
            "format": "password",
            "title": "Mfa Token",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "mfa_token",
          "code"
        ],
        "title": "MfaVerifyRequest",
        "type": "object"
      },
      "NewBuild": {
        "description": "Metadata for a build the caller is about to upload.\n\nPOST /builds/initiate consumes this body, commits a pending upload row,\nand returns a signed upload URL. The actual artifact bytes never travel\nthrough the API.\n\n``original_filename`` is supplied here at initiate time because there's no\nmultipart filename hint to read it from anymore. The server uses it for\nthe Content-Disposition hint at download time.\n\nThere is no ``platform`` field: a project is scoped to a single platform, so the server stamps every build from its project rather than\ntrusting a client-supplied value. A stray ``platform`` in the payload is\nignored, keeping old clients that still send it working.",
        "properties": {
          "build_number": {
            "title": "Build Number",
            "type": "integer"
          },
          "bundle_identifier": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bundle Identifier"
          },
          "original_filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Original Filename"
          },
          "release_notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes"
          },
          "version_name": {
            "title": "Version Name",
            "type": "string"
          }
        },
        "required": [
          "version_name",
          "build_number"
        ],
        "title": "NewBuild",
        "type": "object"
      },
      "NewChannel": {
        "description": "A channel supplied to the API for creation.",
        "properties": {
          "auto_distribute": {
            "default": false,
            "title": "Auto Distribute",
            "type": "boolean"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "notify_on_release": {
            "default": true,
            "title": "Notify On Release",
            "type": "boolean"
          },
          "public": {
            "default": false,
            "title": "Public",
            "type": "boolean"
          }
        },
        "required": [
          "name"
        ],
        "title": "NewChannel",
        "type": "object"
      },
      "NewDeveloper": {
        "description": "Represents a new developer passed to the API for creation.",
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "password": {
            "format": "password",
            "title": "Password",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "name",
          "email",
          "password"
        ],
        "title": "NewDeveloper",
        "type": "object"
      },
      "NewOrganization": {
        "description": "A new organization supplied to the API for creation.\n\n``plan`` and ``onboarding_choice`` select the billing plan and the starting\npath. Both are optional and both default to the existing behaviour: an\nomitted ``plan`` means ``team``, and an omitted ``onboarding_choice`` means\nthe default path, so a payload carrying only ``name`` behaves exactly as it\nalways has.",
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          },
          "onboarding_choice": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OnboardingChoice"
              },
              {
                "type": "null"
              }
            ]
          },
          "plan": {
            "$ref": "#/components/schemas/OrganizationPlan",
            "default": "team"
          }
        },
        "required": [
          "name"
        ],
        "title": "NewOrganization",
        "type": "object"
      },
      "NewOrganizationInvite": {
        "description": "Body of POST /organizations/{id}/invites.",
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          },
          "role": {
            "$ref": "#/components/schemas/OrganizationMemberRole"
          }
        },
        "required": [
          "email",
          "role"
        ],
        "title": "NewOrganizationInvite",
        "type": "object"
      },
      "NewPat": {
        "description": "A PAT supplied to the API for creation.\n\nThe caller picks a human-readable name (shown in UI lists), the scopes,\nand an optional expiration. The token value itself is server-generated\nand returned exactly once in the create response.",
        "properties": {
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "scopes": {
            "$ref": "#/components/schemas/PatScope"
          }
        },
        "required": [
          "name",
          "scopes"
        ],
        "title": "NewPat",
        "type": "object"
      },
      "NewProject": {
        "description": "Represents a new project supplied to the API to be created.",
        "properties": {
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          }
        },
        "required": [
          "name",
          "platform"
        ],
        "title": "NewProject",
        "type": "object"
      },
      "NewProjectAccessToken": {
        "description": "A project access token supplied to the API for creation.\n\nThe caller picks a human-readable name (shown in UI lists), the role the\ntoken should carry, and an optional expiration. The token value itself is\nserver-generated and returned exactly once in the create response.",
        "properties": {
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "granted_role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          },
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name",
          "granted_role"
        ],
        "title": "NewProjectAccessToken",
        "type": "object"
      },
      "NewProjectInvite": {
        "description": "Body of POST /projects/{id}/invites.",
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          },
          "role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          }
        },
        "required": [
          "email",
          "role"
        ],
        "title": "NewProjectInvite",
        "type": "object"
      },
      "NewPublishRequest": {
        "description": "Request body for ``POST /publish``: the build and the store target a publish\njob is created for.\n\nThe ``store_connection_id`` identifies which of the project's connections\n(and therefore which provider) the job targets; ``build_id`` is the artifact\nit references. The service validates both belong to the addressed project\nbefore enqueuing, and derives the provider from the connection. Store\npublishing is not operational, so the job created from this body ends\n``failed`` without the artifact reaching the provider.",
        "properties": {
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "store_connection_id": {
            "format": "uuid",
            "title": "Store Connection Id",
            "type": "string"
          }
        },
        "required": [
          "store_connection_id",
          "build_id"
        ],
        "title": "NewPublishRequest",
        "type": "object"
      },
      "NewRelease": {
        "description": "A release supplied to the API to publish a build on a channel.\n\nThe channel_id is taken from the route path; only the build_id and an\noptional per-channel release-notes override come from the body.",
        "properties": {
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "mandatory_update": {
            "default": false,
            "title": "Mandatory Update",
            "type": "boolean"
          },
          "release_notes_override": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes Override"
          }
        },
        "required": [
          "build_id"
        ],
        "title": "NewRelease",
        "type": "object"
      },
      "NewSdkApp": {
        "description": "An SDK app supplied to the API for creation.\n\nThe caller picks a human-readable name (shown in UI lists). The app_key and\napp_secret are server-generated; the secret is returned exactly once in the\ncreate response.",
        "properties": {
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "title": "NewSdkApp",
        "type": "object"
      },
      "NewStoreConnection": {
        "description": "Create (or replace) a project's connection for one provider.\n\n``credentials`` is a provider-specific object validated by\n``parse_credential`` in the service layer (kept as a raw dict here so a\nsingle endpoint can accept any provider's shape).",
        "properties": {
          "credentials": {
            "additionalProperties": true,
            "title": "Credentials",
            "type": "object"
          },
          "display_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Display Name"
          },
          "provider": {
            "$ref": "#/components/schemas/StoreProvider"
          }
        },
        "required": [
          "provider",
          "credentials"
        ],
        "title": "NewStoreConnection",
        "type": "object"
      },
      "NewWebhookSubscription": {
        "description": "Register a new outbound webhook for an organization.",
        "properties": {
          "active": {
            "default": true,
            "title": "Active",
            "type": "boolean"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "event_types": {
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "title": "Event Types",
            "type": "array"
          },
          "url": {
            "title": "Url",
            "type": "string"
          }
        },
        "required": [
          "url",
          "event_types"
        ],
        "title": "NewWebhookSubscription",
        "type": "object"
      },
      "NewlyCreatedPat": {
        "description": "A PAT returned exactly once at creation, including the raw token value.\n\nThis is the only path through which the caller can ever see the secret;\nonce they navigate away or close the response window, it's gone. The\nserver-side hash is what persists. Re-issuing on demand is not possible \u2014\nif a developer loses their token they create a new one.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "developer_id": {
            "format": "uuid",
            "title": "Developer Id",
            "type": "string"
          },
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_used_at": {
            "format": "date-time",
            "title": "Last Used At",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "scopes": {
            "$ref": "#/components/schemas/PatScope"
          },
          "token": {
            "title": "Token",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "developer_id",
          "organization_id",
          "name",
          "scopes",
          "created_at",
          "last_used_at",
          "token"
        ],
        "title": "NewlyCreatedPat",
        "type": "object"
      },
      "NewlyCreatedProjectAccessToken": {
        "description": "A project access token returned exactly once at creation, with the raw value.\n\nThis is the only path through which the caller can ever see the secret;\nonce they navigate away or close the response, it's gone. The server-side\nhash is what persists. Re-issuing on demand is not possible; a lost token\nis replaced by minting a new one and revoking the old.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "created_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By Developer Id"
          },
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "granted_role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_used_at": {
            "format": "date-time",
            "title": "Last Used At",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "token": {
            "title": "Token",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "project_id",
          "organization_id",
          "name",
          "granted_role",
          "created_at",
          "last_used_at",
          "token"
        ],
        "title": "NewlyCreatedProjectAccessToken",
        "type": "object"
      },
      "NewlyCreatedSdkApp": {
        "description": "An SDK app returned exactly once at creation, including the raw secret.\n\nThis is the only path through which the caller ever sees the secret; once\nthey navigate away it's gone. The server-side hash is what persists. If a\ndeveloper loses the secret they create a new app and revoke the old one.",
        "properties": {
          "app_key": {
            "title": "App Key",
            "type": "string"
          },
          "app_secret": {
            "title": "App Secret",
            "type": "string"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "created_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By Developer Id"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "channel_id",
          "created_by_developer_id",
          "name",
          "app_key",
          "created_at",
          "app_secret"
        ],
        "title": "NewlyCreatedSdkApp",
        "type": "object"
      },
      "OnboardingChoice": {
        "description": "How the creator wants to start the organization.\n\n* ``trial`` \u2014 Team, no card, a seven-day capped trial.\n* ``pay_now`` \u2014 Team with an immediate paid subscription.\n* ``business_card`` \u2014 Business with a card on file for its monthly platform\n  subscription. Business is pre-paid for usage: the card funds the recurring\n  monthly charge, and metered usage draws down pre-paid top-ups.\n\nThe choice is validated against ``plan``: a ``team`` plan accepts ``trial`` or\n``pay_now`` (or neither), and a ``business`` plan accepts ``business_card`` (or\nneither). The two card-backed paths depend on online payment setup, which is\nnot enabled in every environment; where it is unavailable, only the Team trial\nand the default path can provision.",
        "enum": [
          "trial",
          "pay_now",
          "business_card"
        ],
        "title": "OnboardingChoice",
        "type": "string"
      },
      "OnboardingRequestResponse": {
        "description": "An organization's storage-account onboarding request.\n\nCarries the request's current status and, when that status is\n``validation_failed``, a structured ``validation_report`` describing what\nfailed and how to fix it \u2014 the same report ``diagnose`` returns \u2014 so the\nrequest can be corrected and resubmitted.",
        "properties": {
          "account_url": {
            "title": "Account Url",
            "type": "string"
          },
          "container_name": {
            "title": "Container Name",
            "type": "string"
          },
          "created_at": {
            "title": "Created At",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "reject_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reject Reason"
          },
          "request_id": {
            "format": "uuid",
            "title": "Request Id",
            "type": "string"
          },
          "resolved_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Resolved At"
          },
          "status": {
            "enum": [
              "pending",
              "validated",
              "validation_failed",
              "approving",
              "approved",
              "rejected",
              "withdrawn"
            ],
            "title": "Status",
            "type": "string"
          },
          "tenant_id": {
            "format": "uuid",
            "title": "Tenant Id",
            "type": "string"
          },
          "updated_at": {
            "title": "Updated At",
            "type": "string"
          },
          "validation_report": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/BYOSAValidationReport"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "request_id",
          "organization_id",
          "status",
          "tenant_id",
          "account_url",
          "container_name",
          "created_at",
          "updated_at"
        ],
        "title": "OnboardingRequestResponse",
        "type": "object"
      },
      "OnboardingRequestSubmit": {
        "description": "Request body for submitting a storage-account onboarding request.\n\nYou supply only the three identifiers that describe your storage account;\nstatus, submitter and timestamps are set by AppGantry. The identifiers are\nformat-checked before the request is queued, so a malformed value is refused\nrather than stored.",
        "properties": {
          "account_url": {
            "description": "Customer's Azure storage account URL (``https://<account>.blob.core.windows.net``)",
            "title": "Account Url",
            "type": "string"
          },
          "container_name": {
            "description": "Customer's container for BYOSA artefacts",
            "title": "Container Name",
            "type": "string"
          },
          "tenant_id": {
            "description": "Customer's Azure AD tenant ID",
            "format": "uuid",
            "title": "Tenant Id",
            "type": "string"
          }
        },
        "required": [
          "tenant_id",
          "account_url",
          "container_name"
        ],
        "title": "OnboardingRequestSubmit",
        "type": "object"
      },
      "OrgMfaPolicyRequest": {
        "description": "Body for an org ADMIN setting the require-MFA policy.\n\n``required`` turns the policy on or off. ``grace_period_days`` is the window\n(from now) members are given to enrol before they are gated; it is only\nmeaningful when enabling and defaults to 0 (enforce immediately). It is\nignored when ``required`` is False. ``passkey_satisfies_mfa`` selects\nthe login ceremony members may use when the policy is on: True (default) lets\na member sign in with a passwordless passkey; False requires a passkey to be\npresented only as a second factor after a password (passwordless login is\nrefused for members of such an org). It is persisted regardless of\n``required`` so the posture survives a disable/re-enable.",
        "properties": {
          "grace_period_days": {
            "default": 0,
            "title": "Grace Period Days",
            "type": "integer"
          },
          "passkey_satisfies_mfa": {
            "default": true,
            "title": "Passkey Satisfies Mfa",
            "type": "boolean"
          },
          "required": {
            "title": "Required",
            "type": "boolean"
          }
        },
        "required": [
          "required"
        ],
        "title": "OrgMfaPolicyRequest",
        "type": "object"
      },
      "OrgMfaPolicyResponse": {
        "description": "An org's current require-MFA policy, for the org-settings screen.\n\n``enforced_at`` is the moment enforcement begins (now + grace at set time);\nit is None when the policy is off. While ``required`` is True but ``now`` is\nbefore ``enforced_at`` the org is inside its grace window (members can still\nlog in and work while they enrol). ``passkey_satisfies_mfa``\nselects the login ceremony members may use: True means a passwordless passkey\nlogin is a complete MFA login; False means a passkey may only be presented as\na second factor after a password (passwordless login is refused).",
        "properties": {
          "enforced_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Enforced At"
          },
          "passkey_satisfies_mfa": {
            "title": "Passkey Satisfies Mfa",
            "type": "boolean"
          },
          "required": {
            "title": "Required",
            "type": "boolean"
          }
        },
        "required": [
          "required",
          "enforced_at",
          "passkey_satisfies_mfa"
        ],
        "title": "OrgMfaPolicyResponse",
        "type": "object"
      },
      "OrganizationFlagsResponse": {
        "description": "The effective feature-flag set for one organization.\n\n``flags`` maps every active flag key to its resolved value for this\norganization, with any organization-specific setting already applied.\nArchived flags are omitted. Treat a key that is absent as off.",
        "properties": {
          "flags": {
            "additionalProperties": {
              "type": "boolean"
            },
            "title": "Flags",
            "type": "object"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "flags"
        ],
        "title": "OrganizationFlagsResponse",
        "type": "object"
      },
      "OrganizationInvite": {
        "description": "A pending or completed organization_invites row.\n\nThe raw ``invite_token`` is on the wire ONLY for the response to\nPOST /invites \u2014 the caller needs it once to construct the accept\nURL. Subsequent reads (GET /invites listing) omit it. We surface\nthe hash never; the caller's own copy is the only place it lives.",
        "properties": {
          "accepted_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accepted At"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "invite_token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invite Token"
          },
          "invited_at": {
            "format": "date-time",
            "title": "Invited At",
            "type": "string"
          },
          "invited_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invited By Developer Id"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "role": {
            "$ref": "#/components/schemas/OrganizationMemberRole"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "email",
          "role",
          "invited_at",
          "expires_at"
        ],
        "title": "OrganizationInvite",
        "type": "object"
      },
      "OrganizationInvitePage": {
        "description": "Page of pending organization invites.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/OrganizationInvite"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "OrganizationInvitePage",
        "type": "object"
      },
      "OrganizationMember": {
        "description": "An accepted organization membership.\n\n``name`` and ``avatar_url`` carry the member's display identity so a member\nlist can be rendered without a separate read per row. Both are optional for\nforwards compatibility: a client must tolerate their absence. ``name`` is\nalways populated today, and ``avatar_url`` is the developer's avatar URL when\nthey have one, otherwise null.",
        "properties": {
          "avatar_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Avatar Url"
          },
          "developer_id": {
            "format": "uuid",
            "title": "Developer Id",
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "role": {
            "$ref": "#/components/schemas/OrganizationMemberRole"
          }
        },
        "required": [
          "organization_id",
          "developer_id",
          "role"
        ],
        "title": "OrganizationMember",
        "type": "object"
      },
      "OrganizationMemberPage": {
        "description": "Page of organization members.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/OrganizationMember"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "OrganizationMemberPage",
        "type": "object"
      },
      "OrganizationMemberRole": {
        "description": "Represents the roles a developer can have in an organization.",
        "enum": [
          1,
          2,
          1099511627775,
          2199023255552,
          1152921504606846976,
          9223372036854775807
        ],
        "title": "OrganizationMemberRole",
        "type": "integer"
      },
      "OrganizationPlan": {
        "description": "The billing plan chosen when an organization is created.\n\n``team`` is the pre-paid base plan. ``business`` uses the same pre-paid model\n\u2014 a monthly platform charge plus pre-paid top-ups that metered usage draws\ndown. The plan selected here maps onto the organization's tier, which is the\nterm the rest of the billing surface uses.",
        "enum": [
          "team",
          "business"
        ],
        "title": "OrganizationPlan",
        "type": "string"
      },
      "OrganizationResponse": {
        "description": "Wire-format projection of an organization.",
        "properties": {
          "caller_role": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OrganizationMemberRole"
              },
              {
                "type": "null"
              }
            ]
          },
          "card_on_file": {
            "title": "Card On File",
            "type": "boolean"
          },
          "grace_deletion_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Grace Deletion At"
          },
          "grace_started_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Grace Started At"
          },
          "granted_credit_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Granted Credit Cents"
          },
          "has_icon": {
            "title": "Has Icon",
            "type": "boolean"
          },
          "icon_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Icon Url"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "lifecycle_state": {
            "title": "Lifecycle State",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "paid_credit_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Paid Credit Cents"
          },
          "payment_state": {
            "title": "Payment State",
            "type": "string"
          },
          "plan": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OrganizationPlan"
              },
              {
                "type": "null"
              }
            ]
          },
          "trial_credit_remaining_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Trial Credit Remaining Cents"
          },
          "trial_ends_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Trial Ends At"
          }
        },
        "required": [
          "identifier",
          "name",
          "payment_state",
          "lifecycle_state",
          "trial_ends_at",
          "grace_started_at",
          "grace_deletion_at",
          "card_on_file",
          "has_icon",
          "trial_credit_remaining_cents",
          "granted_credit_cents",
          "paid_credit_cents"
        ],
        "title": "OrganizationResponse",
        "type": "object"
      },
      "PasskeySummary": {
        "description": "The safe, API-facing projection of a passkey.\n\nOmits ``credential_id`` and ``public_key`` -- a developer managing their\npasskeys needs the label and timestamps, not the cryptographic material.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "friendly_name": {
            "title": "Friendly Name",
            "type": "string"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_used_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At"
          },
          "transports": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Transports"
          }
        },
        "required": [
          "identifier",
          "friendly_name",
          "created_at"
        ],
        "title": "PasskeySummary",
        "type": "object"
      },
      "PasswordResetConfirm": {
        "description": "Request body for ``POST /developers/reset_password``.\n\nThe ``token`` resolves to a developer_id at consume time via the\nDELETE-RETURNING in ``consume_reset_token``; no developer_id\nneeds to ride in the body.\n\n``new_password`` is bounded identically to the signup path so a\ndeveloper can't reset *to* a password they wouldn't be allowed\nto register *with*.",
        "properties": {
          "new_password": {
            "format": "password",
            "title": "New Password",
            "type": "string",
            "writeOnly": true
          },
          "token": {
            "format": "password",
            "title": "Token",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "token",
          "new_password"
        ],
        "title": "PasswordResetConfirm",
        "type": "object"
      },
      "PasswordResetRequest": {
        "description": "Request body for ``POST /developers/reset_password_request``.\n\nThe route always returns 204 regardless of whether a reset email\nwas actually sent \u2014 see ``send_password_reset_email_by_address``\nfor the anti-enumeration semantics.",
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "PasswordResetRequest",
        "type": "object"
      },
      "PatPage": {
        "description": "Paginated response shape for the PAT list endpoints.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PatResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "PatPage",
        "type": "object"
      },
      "PatResponse": {
        "description": "Wire-format projection of a personal access token. Never carries the raw\ntoken value.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "developer_id": {
            "format": "uuid",
            "title": "Developer Id",
            "type": "string"
          },
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_used_at": {
            "format": "date-time",
            "title": "Last Used At",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "scopes": {
            "$ref": "#/components/schemas/PatScope"
          }
        },
        "required": [
          "identifier",
          "developer_id",
          "organization_id",
          "name",
          "scopes",
          "created_at",
          "last_used_at"
        ],
        "title": "PatResponse",
        "type": "object"
      },
      "PatScope": {
        "description": "Scopes a personal access token can be granted.",
        "enum": [
          1,
          2,
          4,
          8,
          16,
          32,
          9223372036854775807
        ],
        "title": "PatScope",
        "type": "integer"
      },
      "PatchChannel": {
        "description": "A partial update for an existing channel.",
        "properties": {
          "auto_distribute": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Auto Distribute"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "notify_on_release": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Notify On Release"
          },
          "public": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Public"
          }
        },
        "title": "PatchChannel",
        "type": "object"
      },
      "PatchDeveloper": {
        "description": "The model for patching an existing developer.\n\n``name`` is optional (``None`` means \"don't touch\"), matching the PATCH\nconvention the other patch bodies use.",
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          }
        },
        "title": "PatchDeveloper",
        "type": "object"
      },
      "PatchOrganization": {
        "description": "Models changes to an organization.",
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          }
        },
        "title": "PatchOrganization",
        "type": "object"
      },
      "PatchProject": {
        "description": "Represents a patch for an existing project.",
        "properties": {
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          }
        },
        "title": "PatchProject",
        "type": "object"
      },
      "PatchStoreConnection": {
        "description": "Partial update for an existing connection: relabel it and/or rotate its\ncredentials. Both fields optional; a ``None`` means \"leave unchanged\".\n\nNote credentials can only be *replaced* wholesale, never read back \u2014 there\nis no partial-secret edit, by design.",
        "properties": {
          "credentials": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Credentials"
          },
          "display_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Display Name"
          }
        },
        "title": "PatchStoreConnection",
        "type": "object"
      },
      "PatchWebhookSubscription": {
        "description": "Partial update: any subset of url / event_types / description / active.\n\nA ``None`` field means \"leave unchanged\". The signing secret is never edited\nhere \u2014 rotate it with\n``POST /api/v1/organizations/{organization_id}/webhooks/{subscription_id}/rotate-secret``,\nwhich issues a new secret and returns it once.",
        "properties": {
          "active": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Active"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "event_types": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/WebhookEventType"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Event Types"
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url"
          }
        },
        "title": "PatchWebhookSubscription",
        "type": "object"
      },
      "Platform": {
        "description": "The platform a build artifact is compiled for.\n\nScope is mobile and mobile-adjacent (smartwatch, TV, headset) platforms.\nApple-side platforms all share the .ipa upload/distribute flow and only\ndiffer in target SDK; Android-side platforms distribute the installable\n.apk.\n\nValues are a stable part of the wire contract: new platforms may be added\nover time, existing values do not change.",
        "enum": [
          "ios",
          "android",
          "tvos",
          "watchos",
          "visionos",
          "wearos"
        ],
        "title": "Platform",
        "type": "string"
      },
      "PlatformFamily": {
        "description": "The upload/distribute flow a platform belongs to.\n\nApple-side platforms share the ``.ipa`` flow; Android-side platforms\ndistribute the sideloadable ``.apk`` (see ``Platform``). The ``.aab`` App\nBundle is a Play Store *publishing* format rather than a directly\ninstallable artifact, so it is not accepted for distribution uploads; use\nthe Google Play Console for store releases today, because publishing to\nGoogle Play from AppGantry is not operational.",
        "enum": [
          "apple",
          "android"
        ],
        "title": "PlatformFamily",
        "type": "string"
      },
      "PlatformInfo": {
        "description": "One platform's canonical wire shape for the public catalog.\n\n``value`` is the stable ``Platform`` enum value (e.g. ``\"ios\"``) \u2014 the\nidentifier every other API call uses. ``label`` is the human display name\n(e.g. ``\"iOS\"``) every surface renders. ``family`` is the distribution\nfamily the platform's uploads flow through.",
        "properties": {
          "family": {
            "$ref": "#/components/schemas/PlatformFamily"
          },
          "label": {
            "title": "Label",
            "type": "string"
          },
          "value": {
            "title": "Value",
            "type": "string"
          }
        },
        "required": [
          "value",
          "label",
          "family"
        ],
        "title": "PlatformInfo",
        "type": "object"
      },
      "Project": {
        "description": "One app you distribute. A project belongs to exactly one organization\nand targets exactly one platform, chosen at creation.",
        "properties": {
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "icon_content_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Icon Content Type"
          },
          "icon_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Icon Url"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          }
        },
        "required": [
          "organization_id",
          "identifier",
          "name",
          "platform"
        ],
        "title": "Project",
        "type": "object"
      },
      "ProjectAccessTokenPage": {
        "description": "Paginated response shape for the project access token list endpoint.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ProjectAccessTokenResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "ProjectAccessTokenPage",
        "type": "object"
      },
      "ProjectAccessTokenResponse": {
        "description": "Wire-format projection of a project access token. Never carries the secret.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "created_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By Developer Id"
          },
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "granted_role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_used_at": {
            "format": "date-time",
            "title": "Last Used At",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          }
        },
        "required": [
          "identifier",
          "project_id",
          "organization_id",
          "name",
          "granted_role",
          "created_at",
          "last_used_at"
        ],
        "title": "ProjectAccessTokenResponse",
        "type": "object"
      },
      "ProjectDeveloperRole": {
        "description": "Project-scoped permissions a developer can hold.\n\nBits 0-6 are concrete capabilities for the distribution domain.\nMAINTAINER is the convenience role for \"do everything but delete the\nproject\" \u2014 all bits 0..39 inclusive \u2014 and reserves bits 7..39 for\nfiner-grained capabilities the domain model may add later (e.g.\nper-platform or per-channel restrictions). ADMIN is everything.",
        "enum": [
          1,
          2,
          4,
          8,
          16,
          32,
          64,
          1099511627775,
          9223372036854775807
        ],
        "title": "ProjectDeveloperRole",
        "type": "integer"
      },
      "ProjectInvite": {
        "description": "A pending or completed project_invites row.",
        "properties": {
          "accepted_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accepted At"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "invite_token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invite Token"
          },
          "invited_at": {
            "format": "date-time",
            "title": "Invited At",
            "type": "string"
          },
          "invited_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invited By Developer Id"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "email",
          "role",
          "invited_at",
          "expires_at"
        ],
        "title": "ProjectInvite",
        "type": "object"
      },
      "ProjectInvitePage": {
        "description": "Page of pending project invites.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ProjectInvite"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "ProjectInvitePage",
        "type": "object"
      },
      "ProjectMember": {
        "description": "A real (accepted) project membership.\n\n``name`` + ``avatar_url`` mirror ``OrganizationMember`` \u2014 the\ndeveloper's display identity joined in server-side so the WebApp renders the\nmember list without a per-row ``GET /developers/{id}``. Both optional for\nwire-contract forwards-compatibility; the server always populates ``name``\nand sets ``avatar_url`` when an avatar exists.",
        "properties": {
          "avatar_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Avatar Url"
          },
          "creation_date": {
            "format": "date-time",
            "title": "Creation Date",
            "type": "string"
          },
          "developer_id": {
            "format": "uuid",
            "title": "Developer Id",
            "type": "string"
          },
          "last_updated": {
            "format": "date-time",
            "title": "Last Updated",
            "type": "string"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "role": {
            "$ref": "#/components/schemas/ProjectDeveloperRole"
          }
        },
        "required": [
          "organization_id",
          "project_id",
          "developer_id",
          "role",
          "creation_date",
          "last_updated"
        ],
        "title": "ProjectMember",
        "type": "object"
      },
      "ProjectMemberPage": {
        "description": "Page of project members.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ProjectMember"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "ProjectMemberPage",
        "type": "object"
      },
      "PublicReleaseInfo": {
        "description": "The install-facing view of a public channel's current release.\n\nDeliberately minimal: it carries only what an anonymous installer needs \u2014\nthe app version, platform, notes, size, and the relative download path \u2014\nand never the internal release/build provenance (uploader, sha, storage\npath) that the authenticated surfaces expose.",
        "properties": {
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "build_number": {
            "title": "Build Number",
            "type": "integer"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "download_url": {
            "title": "Download Url",
            "type": "string"
          },
          "install_method": {
            "$ref": "#/components/schemas/InstallMethod"
          },
          "install_url": {
            "title": "Install Url",
            "type": "string"
          },
          "ios_manifest_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ios Manifest Url"
          },
          "min_os_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Min Os Version"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "release_notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes"
          },
          "released_at": {
            "format": "date-time",
            "title": "Released At",
            "type": "string"
          },
          "size_bytes": {
            "title": "Size Bytes",
            "type": "integer"
          },
          "version_name": {
            "title": "Version Name",
            "type": "string"
          }
        },
        "required": [
          "channel_id",
          "build_id",
          "platform",
          "version_name",
          "build_number",
          "release_notes",
          "size_bytes",
          "released_at",
          "download_url",
          "install_method",
          "install_url"
        ],
        "title": "PublicReleaseInfo",
        "type": "object"
      },
      "PublishRequest": {
        "description": "A store-publish job, non-secret in full.\n\nEvery field is safe to expose: the store credential is never carried here.\n``provider`` is taken from the store connection when the job is created and\ndoes not change afterwards, so editing the connection later does not rewrite\nthe history of earlier jobs.",
        "properties": {
          "attempts": {
            "default": 0,
            "title": "Attempts",
            "type": "integer"
          },
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "completed_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed At"
          },
          "error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error"
          },
          "external_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "External Ref"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "provider": {
            "$ref": "#/components/schemas/StoreProvider"
          },
          "requested_at": {
            "format": "date-time",
            "title": "Requested At",
            "type": "string"
          },
          "started_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "status": {
            "$ref": "#/components/schemas/PublishStatus"
          },
          "store_connection_id": {
            "format": "uuid",
            "title": "Store Connection Id",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "store_connection_id",
          "provider",
          "build_id",
          "status",
          "requested_at"
        ],
        "title": "PublishRequest",
        "type": "object"
      },
      "PublishRequestPage": {
        "description": "Cursor-paginated page of publish jobs. ``next_cursor`` is the opaque token\nto pass back as ``cursor=...``; null means this was the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PublishRequest"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "PublishRequestPage",
        "type": "object"
      },
      "PublishStatus": {
        "description": "Lifecycle of a store-publish job.\n\n``pending`` -> ``processing`` (worker claimed) ->\n``completed`` (store accepted the artifact, ``external_ref`` may be set) |\n``failed`` (``error`` set). Terminal states are ``completed`` / ``failed``.\n\n``completed`` is part of the future status contract and is not reachable\ntoday: store publishing is not operational, so a job ends ``failed``\nwithout any submission being made to the provider.",
        "enum": [
          "pending",
          "processing",
          "completed",
          "failed"
        ],
        "title": "PublishStatus",
        "type": "string"
      },
      "RecoveryCodesResponse": {
        "description": "The one-time display of a developer's recovery codes.\n\nReturned when enrollment is confirmed and when codes are regenerated. The\nplaintext codes exist only in this response -- the server stores hashes -- so\nthe client must surface them for the user to save; they cannot be re-fetched.",
        "properties": {
          "recovery_codes": {
            "items": {
              "type": "string"
            },
            "title": "Recovery Codes",
            "type": "array"
          }
        },
        "required": [
          "recovery_codes"
        ],
        "title": "RecoveryCodesResponse",
        "type": "object"
      },
      "RefreshRequest": {
        "description": "Request body for refresh / logout. Token is optional in the body since\nthe browser frontend's cookie covers it.",
        "properties": {
          "refresh_token": {
            "anyOf": [
              {
                "format": "password",
                "type": "string",
                "writeOnly": true
              },
              {
                "type": "null"
              }
            ],
            "title": "Refresh Token"
          }
        },
        "title": "RefreshRequest",
        "type": "object"
      },
      "RegistrationBeginResponse": {
        "description": "The server's reply to \"begin registering a passkey\".\n\n``options`` is the WebAuthn ``PublicKeyCredentialCreationOptions`` JSON the\nbrowser feeds straight to ``navigator.credentials.create()``. ``challenge_id``\nis the opaque handle for the server-stored challenge; the client echoes it\nback on finish so the server can find (and single-use consume) the exact\nchallenge this ceremony was started with.",
        "properties": {
          "challenge_id": {
            "format": "uuid",
            "title": "Challenge Id",
            "type": "string"
          },
          "options": {
            "additionalProperties": true,
            "title": "Options",
            "type": "object"
          }
        },
        "required": [
          "challenge_id",
          "options"
        ],
        "title": "RegistrationBeginResponse",
        "type": "object"
      },
      "RegistrationFinishRequest": {
        "description": "The browser's ``navigator.credentials.create()`` result, plus context.\n\n``credential`` is the raw attestation response as produced by the browser;\nit is handed verbatim to the WebAuthn verification library. ``friendly_name``\nis the user-chosen label for the new passkey.",
        "properties": {
          "challenge_id": {
            "format": "uuid",
            "title": "Challenge Id",
            "type": "string"
          },
          "credential": {
            "additionalProperties": true,
            "title": "Credential",
            "type": "object"
          },
          "friendly_name": {
            "title": "Friendly Name",
            "type": "string"
          }
        },
        "required": [
          "challenge_id",
          "credential",
          "friendly_name"
        ],
        "title": "RegistrationFinishRequest",
        "type": "object"
      },
      "ReleaseAnalytics": {
        "description": "Aggregate install/download counters for a single release.\n\nA release is a build distributed on a channel; downloads are recorded per\nbuild, so these counters describe that release's build. ``unique_testers``\ncounts distinct downloading developers \u2014 anonymised rows (developer deleted)\nstill count toward ``total_downloads`` but not ``unique_testers``.",
        "properties": {
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "first_downloaded_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "First Downloaded At"
          },
          "last_downloaded_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Downloaded At"
          },
          "release_id": {
            "format": "uuid",
            "title": "Release Id",
            "type": "string"
          },
          "total_downloads": {
            "title": "Total Downloads",
            "type": "integer"
          },
          "unique_testers": {
            "title": "Unique Testers",
            "type": "integer"
          }
        },
        "required": [
          "release_id",
          "channel_id",
          "build_id",
          "total_downloads",
          "unique_testers",
          "first_downloaded_at",
          "last_downloaded_at"
        ],
        "title": "ReleaseAnalytics",
        "type": "object"
      },
      "ReleasePage": {
        "description": "Paginated response shape for the release list endpoint.\n\nMirrors ``BuildPage``: ``items`` is the page, ``next_cursor`` is\nthe opaque token to pass back. Null next_cursor means \"last\npage\".",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ReleaseResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "ReleasePage",
        "type": "object"
      },
      "ReleaseResponse": {
        "description": "Wire-format projection of a release.",
        "properties": {
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "build_number": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Build Number"
          },
          "build_platform": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Platform"
              },
              {
                "type": "null"
              }
            ]
          },
          "build_version_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Build Version Name"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "deleted_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deleted At"
          },
          "enabled": {
            "default": true,
            "title": "Enabled",
            "type": "boolean"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "mandatory_update": {
            "default": false,
            "title": "Mandatory Update",
            "type": "boolean"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "release_notes_override": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes Override"
          },
          "released_at": {
            "format": "date-time",
            "title": "Released At",
            "type": "string"
          },
          "released_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Released By Developer Id"
          },
          "rolled_back_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rolled Back At"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "channel_id",
          "build_id",
          "released_at"
        ],
        "title": "ReleaseResponse",
        "type": "object"
      },
      "RenamePasskeyRequest": {
        "description": "Request body for renaming a passkey.",
        "properties": {
          "friendly_name": {
            "title": "Friendly Name",
            "type": "string"
          }
        },
        "required": [
          "friendly_name"
        ],
        "title": "RenamePasskeyRequest",
        "type": "object"
      },
      "SSOSessionExchangeRequest": {
        "description": "Request body for the SSO session-handoff exchange.\n\nCarries the single-use code the SAML ACS handed to the WebApp callback in\nthe redirect. The WebApp posts it back here server-to-server to redeem the\nfreshly authenticated session.",
        "properties": {
          "code": {
            "format": "password",
            "title": "Code",
            "type": "string",
            "writeOnly": true
          }
        },
        "required": [
          "code"
        ],
        "title": "SSOSessionExchangeRequest",
        "type": "object"
      },
      "SdkAppMetadata": {
        "description": "Display metadata for the SDK update prompt.\n\n``has_release`` distinguishes \"no published release for this platform\" (all\nthe ``latest_*`` fields and ``icon_url`` are ``None``) from a real release.\n\n``icon_url``, when present, is an absolute URL on the public image CDN. It is\nfetched **without** the update token \u2014 it is not part of the ``/sdk/v1``\nsurface. Icons are extracted from the build when it is uploaded, for Apple and\nAndroid artifacts alike, but extraction does not always succeed, so\n``icon_url`` can be ``None`` on a release that otherwise exists. Treat a\nmissing icon as normal and fall back to your own asset.",
        "properties": {
          "has_release": {
            "title": "Has Release",
            "type": "boolean"
          },
          "icon_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Icon Url"
          },
          "latest_build_number": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Build Number"
          },
          "latest_version_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Version Name"
          },
          "min_os_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Min Os Version"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          }
        },
        "required": [
          "name",
          "platform",
          "has_release"
        ],
        "title": "SdkAppMetadata",
        "type": "object"
      },
      "SdkAppPage": {
        "description": "Paginated response shape for the SDK-app list endpoint.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/SdkAppResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "SdkAppPage",
        "type": "object"
      },
      "SdkAppResponse": {
        "description": "Wire-format projection of an SDK app registration (never carries the\nsecret).",
        "properties": {
          "app_key": {
            "title": "App Key",
            "type": "string"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "created_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By Developer Id"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "channel_id",
          "created_by_developer_id",
          "name",
          "app_key",
          "created_at"
        ],
        "title": "SdkAppResponse",
        "type": "object"
      },
      "SdkEnrollRequest": {
        "description": "Body of ``POST /sdk/v1/apps/{app_key}/enroll``.\n\nThe app secret is what entitles an install to enroll. It is issued once, with\nthe app key, when the SDK app is created.",
        "properties": {
          "app_secret": {
            "title": "App Secret",
            "type": "string"
          }
        },
        "required": [
          "app_secret"
        ],
        "title": "SdkEnrollRequest",
        "type": "object"
      },
      "SdkInstallReportRequest": {
        "description": "A single state report for the caller's install of a release.\n\n``error_message`` is only meaningful for the ``failed`` state (a short,\ntruncated diagnostic the SDK captured); it's ignored/accepted-as-null for the\nothers. The release being reported on is the path ``release_id`` \u2014 the build\nnumber is derived server-side from that release, so a client can't misattribute\na report to a build it isn't actually installing.",
        "properties": {
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message"
          },
          "state": {
            "$ref": "#/components/schemas/SdkInstallState"
          }
        },
        "required": [
          "state"
        ],
        "title": "SdkInstallReportRequest",
        "type": "object"
      },
      "SdkInstallState": {
        "description": "The lifecycle state an SDK install reports for a release.\n\nEach value is what the client says its own install flow reached, not\nsomething observed on the device: ``downloading`` while it is fetching the\nrelease artifact, ``installing`` once it has handed the install off,\n``installed`` when it has confirmed the update is in place, and ``failed``\nif its flow did not get that far. Reporting is optional. The values are a\nstable part of the contract: new ones may be added, existing ones do not\nchange.",
        "enum": [
          "downloading",
          "installing",
          "installed",
          "failed"
        ],
        "title": "SdkInstallState",
        "type": "string"
      },
      "SdkInstallToken": {
        "description": "The scoped update token minted at enroll / refresh.\n\n``install_id`` is the stable identity of this install across token\nrotations; ``update_token`` is the raw secret the SDK stores and presents on\nsubsequent SDK calls. It is shown exactly once per mint.",
        "properties": {
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "install_id": {
            "format": "uuid",
            "title": "Install Id",
            "type": "string"
          },
          "update_token": {
            "title": "Update Token",
            "type": "string"
          }
        },
        "required": [
          "install_id",
          "update_token"
        ],
        "title": "SdkInstallToken",
        "type": "object"
      },
      "SdkUpdateInfo": {
        "description": "The result of an SDK check-for-update poll.\n\nWhen no release is published for the platform, ``release_id`` (and the other\n``latest_*`` fields) are ``None`` and both ``update_available`` and\n``already_current`` are ``False`` \u2014 there is simply nothing to install.",
        "properties": {
          "already_current": {
            "title": "Already Current",
            "type": "boolean"
          },
          "download_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Download Url"
          },
          "ios_manifest_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ios Manifest Url"
          },
          "is_mandatory": {
            "title": "Is Mandatory",
            "type": "boolean"
          },
          "latest_build_number": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Build Number"
          },
          "latest_version_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Version Name"
          },
          "platform": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Platform"
              },
              {
                "type": "null"
              }
            ]
          },
          "release_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Id"
          },
          "release_notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes"
          },
          "released_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Released At"
          },
          "size_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Size Bytes"
          },
          "update_available": {
            "title": "Update Available",
            "type": "boolean"
          }
        },
        "required": [
          "update_available",
          "already_current",
          "is_mandatory"
        ],
        "title": "SdkUpdateInfo",
        "type": "object"
      },
      "SelfServeTierChangeRequest": {
        "description": "Body for the customer self-serve tier change.\n\n``target_tier`` is the tier the org admin wants to move their own org to:\n``business`` runs the upgrade orchestration (or, for an uncarded org,\nreturns a card-capture checkout), ``team`` the downgrade. There is no\noperator attribution: the audit ``editor_id`` is always the acting\ndeveloper for a self-serve change.",
        "properties": {
          "target_tier": {
            "description": "The tier to move the org to.",
            "enum": [
              "team",
              "business"
            ],
            "title": "Target Tier",
            "type": "string"
          }
        },
        "required": [
          "target_tier"
        ],
        "title": "SelfServeTierChangeRequest",
        "type": "object"
      },
      "SelfServeTierChangeResult": {
        "description": "Outcome of a self-serve (customer-initiated) tier change.\n\nA self-serve request has two shapes depending on whether the change could\ncomplete synchronously:\n\n* ``status='changed'`` - the tier flipped now (a downgrade, or an upgrade\n  of an org that already has a card on file). ``change`` carries the\n  ``TierChangeResult``; ``checkout`` is null.\n* ``status='checkout_required'`` - an uncarded Team org asked to upgrade to\n  Business, so it must first add a payment method. Business is pre-paid like\n  Team (usage draws down the organization's credit balance), but it carries a\n  higher monthly platform fee charged in advance to a card on file, so the\n  upgrade cannot complete until one is captured. ``checkout``\n  carries the Paddle card-capture checkout for the frontend to open; the\n  tier is unchanged. Completing the upgrade after the card lands is\n  webhook-driven and tracked as a follow-up child task; ``change`` is null.\n\nExactly one of ``change`` / ``checkout`` is populated for a given status.",
        "properties": {
          "change": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TierChangeResult"
              },
              {
                "type": "null"
              }
            ]
          },
          "checkout": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CardCaptureResponse"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "enum": [
              "changed",
              "checkout_required"
            ],
            "title": "Status",
            "type": "string"
          }
        },
        "required": [
          "status"
        ],
        "title": "SelfServeTierChangeResult",
        "type": "object"
      },
      "SessionResponse": {
        "description": "One active session, as returned to its owning developer.",
        "properties": {
          "client_subnet": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Subnet"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_used_at": {
            "format": "date-time",
            "title": "Last Used At",
            "type": "string"
          },
          "user_agent": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "User Agent"
          }
        },
        "required": [
          "identifier",
          "created_at",
          "last_used_at",
          "expires_at"
        ],
        "title": "SessionResponse",
        "type": "object"
      },
      "SpendCapView": {
        "description": "The org's spend-cap posture, for the customer org-settings screen.\n\nSurfaces both caps and the effective cap the pre-mint gate actually enforces\n(``min`` of the two, ignoring NULLs). ``operator_spend_cap_cents`` is the\noperator credit-risk ceiling (read-only to the customer); ``customer_spend_cap_cents``\nis the customer's own self-limit (settable by an org admin). ``effective_spend_cap_cents``\nis what binds: null only when BOTH are unset (unlimited monthly).",
        "properties": {
          "customer_spend_cap_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Customer Spend Cap Cents"
          },
          "effective_spend_cap_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Effective Spend Cap Cents"
          },
          "operator_spend_cap_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Operator Spend Cap Cents"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "operator_spend_cap_cents",
          "customer_spend_cap_cents",
          "effective_spend_cap_cents"
        ],
        "title": "SpendCapView",
        "type": "object"
      },
      "SsoConnectionRequest": {
        "description": "Body for configuring (create or replace) an org's SAML SSO connection.\n\nA full replace, not a partial patch: every field is applied. ``enabled``\ndefaults to true so the common \"configure and turn on\" flow is one call; set\nit false to stage config before going live.",
        "properties": {
          "email_attribute": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "SAML attribute carrying the developer's email. Null => the NameID is the email.",
            "title": "Email Attribute"
          },
          "enabled": {
            "default": true,
            "description": "Whether SSO is live for the org.",
            "title": "Enabled",
            "type": "boolean"
          },
          "idp_certificate": {
            "description": "The IdP's public X.509 signing certificate (PEM).",
            "title": "Idp Certificate",
            "type": "string"
          },
          "idp_entity_id": {
            "description": "The IdP's SAML EntityID (expected assertion Issuer).",
            "title": "Idp Entity Id",
            "type": "string"
          },
          "idp_slo_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The IdP Single-Logout endpoint (must be https). Null disables SP-initiated logout.",
            "title": "Idp Slo Url"
          },
          "idp_sso_url": {
            "description": "The IdP Single-Sign-On endpoint (must be https).",
            "title": "Idp Sso Url",
            "type": "string"
          },
          "name_attribute": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "SAML attribute carrying the developer's display name (optional).",
            "title": "Name Attribute"
          },
          "nameid_format": {
            "default": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
            "description": "The SAML NameID format the SP requests / expects.",
            "title": "Nameid Format",
            "type": "string"
          }
        },
        "required": [
          "idp_entity_id",
          "idp_sso_url",
          "idp_certificate"
        ],
        "title": "SsoConnectionRequest",
        "type": "object"
      },
      "SsoConnectionResponse": {
        "description": "The org's stored SSO connection plus the derived SP (our-side) endpoints.\n\nThe Service Provider fields (``sp_*``) are what the customer configures in\ntheir IdP: our EntityID, the Assertion Consumer Service (ACS) URL the IdP\nPOSTs the assertion to, and the SP metadata URL. They are derived from the\npublic API base URL and the org id, not stored.",
        "properties": {
          "email_attribute": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Email Attribute"
          },
          "enabled": {
            "title": "Enabled",
            "type": "boolean"
          },
          "idp_certificate": {
            "title": "Idp Certificate",
            "type": "string"
          },
          "idp_entity_id": {
            "title": "Idp Entity Id",
            "type": "string"
          },
          "idp_slo_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Idp Slo Url"
          },
          "idp_sso_url": {
            "title": "Idp Sso Url",
            "type": "string"
          },
          "name_attribute": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name Attribute"
          },
          "nameid_format": {
            "title": "Nameid Format",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "require_sso": {
            "title": "Require Sso",
            "type": "boolean"
          },
          "sp_acs_url": {
            "title": "Sp Acs Url",
            "type": "string"
          },
          "sp_entity_id": {
            "title": "Sp Entity Id",
            "type": "string"
          },
          "sp_metadata_url": {
            "title": "Sp Metadata Url",
            "type": "string"
          },
          "sp_slo_url": {
            "title": "Sp Slo Url",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "idp_entity_id",
          "idp_sso_url",
          "idp_certificate",
          "idp_slo_url",
          "nameid_format",
          "email_attribute",
          "name_attribute",
          "enabled",
          "require_sso",
          "sp_entity_id",
          "sp_acs_url",
          "sp_slo_url",
          "sp_metadata_url"
        ],
        "title": "SsoConnectionResponse",
        "type": "object"
      },
      "SsoDomainListResponse": {
        "description": "All of an org's email-domain claims (verified and pending).",
        "properties": {
          "domains": {
            "items": {
              "$ref": "#/components/schemas/SsoDomainResponse"
            },
            "title": "Domains",
            "type": "array"
          }
        },
        "required": [
          "domains"
        ],
        "title": "SsoDomainListResponse",
        "type": "object"
      },
      "SsoDomainResponse": {
        "description": "An org's email-domain claim plus the DNS TXT record proving it.\n\n``verification_token`` and the ``txt_record_*`` fields tell the customer\nexactly what to publish in DNS: a TXT record at ``txt_record_name`` with\nvalue ``txt_record_value``. Once our DNS lookup observes that record the\nclaim flips to ``verified``. The token is a single-use ownership nonce, not\na secret credential.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "domain": {
            "title": "Domain",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "txt_record_name": {
            "title": "Txt Record Name",
            "type": "string"
          },
          "txt_record_value": {
            "title": "Txt Record Value",
            "type": "string"
          },
          "verification_token": {
            "title": "Verification Token",
            "type": "string"
          },
          "verified": {
            "title": "Verified",
            "type": "boolean"
          },
          "verified_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Verified At"
          }
        },
        "required": [
          "organization_id",
          "domain",
          "verified",
          "verification_token",
          "txt_record_name",
          "txt_record_value",
          "created_at",
          "verified_at"
        ],
        "title": "SsoDomainResponse",
        "type": "object"
      },
      "SsoDomainVerificationRequest": {
        "description": "Body for claiming an email domain the org intends to prove ownership of.\n\nThe domain is normalised to lower case and any trailing dot is dropped\nbefore validation, so ``Example.COM.`` and ``example.com`` are the same\nclaim. Only a bare registrable domain or subdomain is accepted (no scheme,\nport, path, or wildcard).",
        "properties": {
          "domain": {
            "description": "The email domain to claim (e.g. example.com).",
            "title": "Domain",
            "type": "string"
          }
        },
        "required": [
          "domain"
        ],
        "title": "SsoDomainVerificationRequest",
        "type": "object"
      },
      "SsoEnabledRequest": {
        "description": "Body for toggling an org's SSO enabled flag without re-sending config.",
        "properties": {
          "enabled": {
            "description": "Whether SSO should be live for the org.",
            "title": "Enabled",
            "type": "boolean"
          }
        },
        "required": [
          "enabled"
        ],
        "title": "SsoEnabledRequest",
        "type": "object"
      },
      "SsoLogoutResponse": {
        "description": "The identity provider redirect URL that begins SP-initiated SAML Single\nLogout.\n\nThe web app drives the browser: it ends its own session, then redirects the\nbrowser to ``redirect_url`` so the identity provider tears down its session\nand returns a LogoutResponse to the AppGantry Single Logout endpoint.",
        "properties": {
          "redirect_url": {
            "description": "The IdP Single-Logout URL to redirect the browser to.",
            "title": "Redirect Url",
            "type": "string"
          }
        },
        "required": [
          "redirect_url"
        ],
        "title": "SsoLogoutResponse",
        "type": "object"
      },
      "SsoRequireRequest": {
        "description": "Body for toggling an organization's require-SSO enforcement policy.\n\nWhile it is enabled, members whose email domain the organization has verified\nmust sign in through the identity provider. Enabling requires an enabled\nconnection and at least one verified domain; disabling is always allowed.",
        "properties": {
          "require_sso": {
            "description": "Whether members must authenticate via SSO.",
            "title": "Require Sso",
            "type": "boolean"
          }
        },
        "required": [
          "require_sso"
        ],
        "title": "SsoRequireRequest",
        "type": "object"
      },
      "StoreConnection": {
        "description": "A store connection as returned by the API.\n\nCarries NO secret material \u2014 only the ``non_secret`` projection (provider\nidentifiers safe to display) and lifecycle metadata. This is what the read\nroutes serve; the sealed credential is never returned.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "display_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Display Name"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "non_secret": {
            "additionalProperties": true,
            "default": {},
            "title": "Non Secret",
            "type": "object"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "provider": {
            "$ref": "#/components/schemas/StoreProvider"
          },
          "updated_at": {
            "format": "date-time",
            "title": "Updated At",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "project_id",
          "provider",
          "created_at",
          "updated_at"
        ],
        "title": "StoreConnection",
        "type": "object"
      },
      "StoreConnectionPage": {
        "description": "Cursor-paginated page of store connections. ``next_cursor`` is the opaque\ntoken to pass back as ``cursor=...``; null means this was the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/StoreConnection"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "StoreConnectionPage",
        "type": "object"
      },
      "StoreProvider": {
        "description": "The external publishing targets we model. Values are a stable part of the\nwire contract: new providers may be added, existing values do not change.",
        "enum": [
          "app_store_connect",
          "google_play",
          "intune"
        ],
        "title": "StoreProvider",
        "type": "string"
      },
      "TesterAccessTier": {
        "description": "Which tier of grant gives a developer access to a resource.\n\nUsed on the tester-facing /testers/me/channels listing \u2014 each\nchannel a developer can access reports the highest-up tier of\ngrant they have, so the UI can show \"via your org membership\"\nvs \"via project membership\" vs \"direct channel grant.\"",
        "enum": [
          "organization",
          "project",
          "channel"
        ],
        "title": "TesterAccessTier",
        "type": "string"
      },
      "TesterAndroidInstallLink": {
        "description": "Response of ``GET /testers/me/releases/{id}/android-install-link``.\n\nThe Android counterpart to ``TesterIosInstallLink``. Android has no\nitms-services manifest indirection \u2014 the device installs straight from the\nAPK \u2014 so ``install_url`` is the absolute HTTPS **direct download** URL for\nthe .apk, carrying the same short-lived capability token the iOS manifest\nembeds. The tester's browser fetches it session-lessly and hands the .apk to\nthe package installer (given \"install from unknown sources\"). There is\ndeliberately no ``manifest_url``. ``expires_in`` is the token lifetime in\nseconds \u2014 open the link promptly.",
        "properties": {
          "expires_in": {
            "title": "Expires In",
            "type": "integer"
          },
          "install_url": {
            "title": "Install Url",
            "type": "string"
          }
        },
        "required": [
          "install_url",
          "expires_in"
        ],
        "title": "TesterAndroidInstallLink",
        "type": "object"
      },
      "TesterChannelSummary": {
        "description": "One channel in GET /testers/me/channels, annotated with the access tier.",
        "properties": {
          "access_tier": {
            "$ref": "#/components/schemas/TesterAccessTier"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "channel_name": {
            "title": "Channel Name",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "organization_name": {
            "title": "Organization Name",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "project_name": {
            "title": "Project Name",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "organization_name",
          "project_id",
          "project_name",
          "channel_id",
          "channel_name",
          "access_tier"
        ],
        "title": "TesterChannelSummary",
        "type": "object"
      },
      "TesterDetailResponse": {
        "description": "Single-tester detail: the tester's direct active membership at the queried\ntier plus the org-level tester groups they belong to.\n\nKeyed on the *direct* active membership (the same entry the tester list\nshows), so a developer who only has group-conferred access \u2014 and\ntherefore never appears as a direct list entry \u2014 is a uniform 404,\nmatching the list/DELETE surfaces. ``groups`` is supplementary context,\nnever the thing that makes the lookup succeed.",
        "properties": {
          "developer_id": {
            "format": "uuid",
            "title": "Developer Id",
            "type": "string"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "granted_at": {
            "format": "date-time",
            "title": "Granted At",
            "type": "string"
          },
          "granted_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Granted By Developer Id"
          },
          "groups": {
            "default": [],
            "items": {
              "$ref": "#/components/schemas/TesterGroupMembershipRef"
            },
            "title": "Groups",
            "type": "array"
          },
          "state": {
            "title": "State",
            "type": "string"
          }
        },
        "required": [
          "developer_id",
          "email",
          "state",
          "granted_at"
        ],
        "title": "TesterDetailResponse",
        "type": "object"
      },
      "TesterGroupGrantResponse": {
        "description": "A tester-group scope grant on the wire.",
        "properties": {
          "channel_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Channel Id"
          },
          "granted_at": {
            "format": "date-time",
            "title": "Granted At",
            "type": "string"
          },
          "granted_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Granted By Developer Id"
          },
          "group_id": {
            "format": "uuid",
            "title": "Group Id",
            "type": "string"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "project_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project Id"
          },
          "scope_type": {
            "$ref": "#/components/schemas/TesterAccessTier"
          }
        },
        "required": [
          "identifier",
          "group_id",
          "scope_type",
          "granted_at"
        ],
        "title": "TesterGroupGrantResponse",
        "type": "object"
      },
      "TesterGroupMembershipRef": {
        "description": "One tester group (within the queried organization) a tester belongs to.\n\nSurfaces the \"grant source\" the tester detail page needs: whether a\ntester's access is only their direct grant or is also (or instead)\nconferred through group membership. Group membership is org-level, so the\nsame refs are returned at both org and project tier.",
        "properties": {
          "group_id": {
            "format": "uuid",
            "title": "Group Id",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "group_id",
          "name"
        ],
        "title": "TesterGroupMembershipRef",
        "type": "object"
      },
      "TesterGroupPage": {
        "description": "One cursor-paginated page of an org's tester groups.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/TesterGroupResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "TesterGroupPage",
        "type": "object"
      },
      "TesterGroupResponse": {
        "description": "A tester group on the wire.",
        "properties": {
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "created_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By Developer Id"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          },
          "notify_on_release": {
            "title": "Notify On Release",
            "type": "boolean"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "name",
          "notify_on_release",
          "created_at"
        ],
        "title": "TesterGroupResponse",
        "type": "object"
      },
      "TesterInstallStatus": {
        "description": "One channel-direct tester's install status for a release's build.\n\n``granted_at`` is the channel-tier grant timestamp; together with\n``developer_id`` it forms the keyset cursor for the listing.",
        "properties": {
          "developer_id": {
            "format": "uuid",
            "title": "Developer Id",
            "type": "string"
          },
          "download_count": {
            "title": "Download Count",
            "type": "integer"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "granted_at": {
            "format": "date-time",
            "title": "Granted At",
            "type": "string"
          },
          "installed": {
            "title": "Installed",
            "type": "boolean"
          },
          "last_downloaded_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Downloaded At"
          }
        },
        "required": [
          "developer_id",
          "email",
          "granted_at",
          "installed",
          "download_count",
          "last_downloaded_at"
        ],
        "title": "TesterInstallStatus",
        "type": "object"
      },
      "TesterInstallStatusPage": {
        "description": "A cursor-paginated page of per-tester install status.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/TesterInstallStatus"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items",
          "next_cursor"
        ],
        "title": "TesterInstallStatusPage",
        "type": "object"
      },
      "TesterIosInstallLink": {
        "description": "Response of ``GET /testers/me/releases/{id}/ios-install-link``.\n\nThe authenticated, entitlement-checked mint of an iOS OTA install link for a\nprivate-channel release. ``install_url`` is the ``itms-services://`` link the\ntester's device opens to install; ``manifest_url`` is the absolute HTTPS\n``manifest.plist`` URL it embeds (both carry the short-lived capability\ntoken). ``expires_in`` is the token lifetime in seconds \u2014 the tester should\nopen the link promptly.",
        "properties": {
          "expires_in": {
            "title": "Expires In",
            "type": "integer"
          },
          "install_url": {
            "title": "Install Url",
            "type": "string"
          },
          "manifest_url": {
            "title": "Manifest Url",
            "type": "string"
          }
        },
        "required": [
          "install_url",
          "manifest_url",
          "expires_in"
        ],
        "title": "TesterIosInstallLink",
        "type": "object"
      },
      "TesterMemberPage": {
        "description": "One cursor-paginated page of a GET /.../testers listing.\n\nThe merged active+pending list is now an {items, next_cursor} envelope\n(replacing the bare array silently truncated at 1000) so a tier with >1000\ntesters is reachable page by page. ``next_cursor`` is null on the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/TesterMemberSummary"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "TesterMemberPage",
        "type": "object"
      },
      "TesterMemberSummary": {
        "description": "One member in a GET /.../testers listing.\n\nUsed at all three tiers. ``state`` distinguishes active members\nfrom pending invites: admins see both in the same list so they\ncan revoke a pending invite the same way they remove an\naccepted member.\n\n``identifier`` is the stable per-invite id of a *pending* grant\n(NULL for active members, who are addressed by ``developer_id``).\nIt's what the admin UI POSTs to the cancel endpoint\n(``DELETE /.../testers/pending/{pending_id}``); without it a pending\norg/project invite could be created but never cancelled.",
        "properties": {
          "developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Developer Id"
          },
          "email": {
            "title": "Email",
            "type": "string"
          },
          "granted_at": {
            "format": "date-time",
            "title": "Granted At",
            "type": "string"
          },
          "granted_by_developer_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Granted By Developer Id"
          },
          "identifier": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Identifier"
          },
          "state": {
            "title": "State",
            "type": "string"
          }
        },
        "required": [
          "email",
          "granted_at",
          "state"
        ],
        "title": "TesterMemberSummary",
        "type": "object"
      },
      "TesterReleaseListItem": {
        "description": "One installable release in GET /testers/me/releases.\n\nCarries the channel context (so a tester sees which app/channel the\nbuild belongs to), the access tier that confers it, the per-build\ndisplay metadata the install prompt needs (version, notes, size),\nand a relative ``install_url`` pointing at the tester download route\nfor the build. The URL is relative on purpose: the public API base\nhost varies per deployment, and the caller is already talking to it,\nso a path keeps the response host-agnostic.\n\n``install_method`` discriminates how the caller acts on the\nbuild: DIRECT_DOWNLOAD means fetch ``install_url`` straight away (Android,\nnon-OTA builds); OTA_MANIFEST means the build installs over-the-air, so the\ncaller mints a tokenised itms-services link via\n``GET /testers/me/releases/{id}/ios-install-link`` rather than using\n``install_url`` directly. ``install_url`` stays populated (the direct\ndownload route) in both cases so a non-OTA fallback path is always available.",
        "properties": {
          "access_tier": {
            "$ref": "#/components/schemas/TesterAccessTier"
          },
          "build_id": {
            "format": "uuid",
            "title": "Build Id",
            "type": "string"
          },
          "build_number": {
            "title": "Build Number",
            "type": "integer"
          },
          "channel_id": {
            "format": "uuid",
            "title": "Channel Id",
            "type": "string"
          },
          "channel_name": {
            "title": "Channel Name",
            "type": "string"
          },
          "install_method": {
            "$ref": "#/components/schemas/InstallMethod"
          },
          "install_url": {
            "title": "Install Url",
            "type": "string"
          },
          "mandatory_update": {
            "default": false,
            "title": "Mandatory Update",
            "type": "boolean"
          },
          "min_os_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Min Os Version"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "organization_name": {
            "title": "Organization Name",
            "type": "string"
          },
          "original_filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Original Filename"
          },
          "platform": {
            "title": "Platform",
            "type": "string"
          },
          "project_id": {
            "format": "uuid",
            "title": "Project Id",
            "type": "string"
          },
          "project_name": {
            "title": "Project Name",
            "type": "string"
          },
          "release_id": {
            "format": "uuid",
            "title": "Release Id",
            "type": "string"
          },
          "release_notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes"
          },
          "released_at": {
            "format": "date-time",
            "title": "Released At",
            "type": "string"
          },
          "size_bytes": {
            "title": "Size Bytes",
            "type": "integer"
          },
          "version_name": {
            "title": "Version Name",
            "type": "string"
          }
        },
        "required": [
          "organization_id",
          "organization_name",
          "project_id",
          "project_name",
          "channel_id",
          "channel_name",
          "access_tier",
          "platform",
          "release_id",
          "build_id",
          "version_name",
          "build_number",
          "size_bytes",
          "released_at",
          "install_url",
          "install_method"
        ],
        "title": "TesterReleaseListItem",
        "type": "object"
      },
      "TierChangeResult": {
        "description": "Outcome of a tier-change orchestration (upgrade/downgrade).\n\n``outcome`` is the discriminated status (``changed`` / ``noop``); a\nrefused or invalid transition surfaces as an error response rather than\na result here. ``paddle_plan_changed`` and\n``usage_settled`` report which optional side effects actually ran (both\nno-op when Paddle is unconfigured or the org has no subscription), so the\ncaller/route can report exactly what happened.",
        "properties": {
          "new_tier": {
            "title": "New Tier",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "outcome": {
            "title": "Outcome",
            "type": "string"
          },
          "paddle_plan_changed": {
            "title": "Paddle Plan Changed",
            "type": "boolean"
          },
          "usage_settled": {
            "title": "Usage Settled",
            "type": "boolean"
          }
        },
        "required": [
          "organization_id",
          "new_tier",
          "outcome",
          "paddle_plan_changed",
          "usage_settled"
        ],
        "title": "TierChangeResult",
        "type": "object"
      },
      "TopUpRequest": {
        "description": "Request to add prepaid credit.\n\n``amount_cents`` must fall between the configured minimum and a fixed policy\nceiling; an out-of-range value is a clean 400.",
        "properties": {
          "amount_cents": {
            "description": "Credit to add, in integer cents (positive, <= $10M).",
            "exclusiveMinimum": 0.0,
            "maximum": 1000000000.0,
            "title": "Amount Cents",
            "type": "integer"
          }
        },
        "required": [
          "amount_cents"
        ],
        "title": "TopUpRequest",
        "type": "object"
      },
      "TopUpResponse": {
        "description": "The minted Paddle checkout for a top-up.\n\n``transaction_id`` is what the frontend opens with Paddle.js; ``checkout_url``\nis the hosted-checkout link when Paddle exposes one (else null \u2014 use the id).\n``amount_cents`` echoes the credit that WILL be applied once the payment\ncompletes and the webhook lands; the balance is unchanged at this point.",
        "properties": {
          "amount_cents": {
            "title": "Amount Cents",
            "type": "integer"
          },
          "checkout_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Checkout Url"
          },
          "transaction_id": {
            "title": "Transaction Id",
            "type": "string"
          }
        },
        "required": [
          "transaction_id",
          "checkout_url",
          "amount_cents"
        ],
        "title": "TopUpResponse",
        "type": "object"
      },
      "TotpEnrollmentBeginResponse": {
        "description": "Reply to \"begin TOTP enrollment\".\n\n``secret`` is the base32 shared secret for manual entry; ``provisioning_uri``\nis the ``otpauth://`` URI the WebApp renders as a QR code. Both encode the\nsame secret -- the client shows whichever the user's app can consume. The\nsecret is returned exactly once, at enrollment; it is never readable again.",
        "properties": {
          "provisioning_uri": {
            "title": "Provisioning Uri",
            "type": "string"
          },
          "secret": {
            "title": "Secret",
            "type": "string"
          }
        },
        "required": [
          "secret",
          "provisioning_uri"
        ],
        "title": "TotpEnrollmentBeginResponse",
        "type": "object"
      },
      "TotpEnrollmentConfirmRequest": {
        "description": "Body for confirming TOTP enrollment: the first code from the app.",
        "properties": {
          "code": {
            "title": "Code",
            "type": "string"
          }
        },
        "required": [
          "code"
        ],
        "title": "TotpEnrollmentConfirmRequest",
        "type": "object"
      },
      "TransferOrganizationOwnership": {
        "description": "Body of POST /organizations/{id}/transfer-ownership.\n\nNames the existing member to grant ADMIN (co-ownership) to. Ownership in\nthis model is the ADMIN role flag (there is no single-owner column and\nmultiple admins are allowed), so a \"transfer\" is a promotion of another\nmember to ADMIN. Promote-only: the initiating admin keeps their own role\nand steps down separately.",
        "properties": {
          "new_owner_developer_id": {
            "format": "uuid",
            "title": "New Owner Developer Id",
            "type": "string"
          }
        },
        "required": [
          "new_owner_developer_id"
        ],
        "title": "TransferOrganizationOwnership",
        "type": "object"
      },
      "UpdateBuildReleaseNotes": {
        "description": "Body for the metadata-only build edit (``PATCH /builds/{build_id}``).\n\n``release_notes`` is the ONLY editable field on an otherwise-immutable\nbuild: it is human-facing commentary, not part of the artifact identity.\nThe request carries just this field so the endpoint cannot be used to\nalter ``sha256``, ``version_name``, ``build_number``,\n``bundle_identifier`` or any other identity attribute. An explicit\n``null`` clears the notes; the value is bounded by the same\n10,000-character cap as ``NewBuild.release_notes``.",
        "properties": {
          "release_notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Release Notes"
          }
        },
        "title": "UpdateBuildReleaseNotes",
        "type": "object"
      },
      "UpdateTesterGroupRequest": {
        "description": "Body of PATCH /organizations/{id}/tester-groups/{group_id}.\n\nBoth fields are optional so a caller can rename, flip the release-notification\ntoggle, or both in one PATCH. At least one must be present (an empty PATCH is\nrejected rather than silently no-op'ing).",
        "properties": {
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "notify_on_release": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Notify On Release"
          }
        },
        "title": "UpdateTesterGroupRequest",
        "type": "object"
      },
      "UploadCompletion": {
        "description": "Request body for finalising a direct upload.",
        "properties": {
          "sha256": {
            "title": "Sha256",
            "type": "string"
          }
        },
        "required": [
          "sha256"
        ],
        "title": "UploadCompletion",
        "type": "object"
      },
      "UploadInitiation": {
        "description": "Response shape for /builds/initiate.",
        "properties": {
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "pending_upload_id": {
            "format": "uuid",
            "title": "Pending Upload Id",
            "type": "string"
          },
          "upload_url": {
            "title": "Upload Url",
            "type": "string"
          }
        },
        "required": [
          "pending_upload_id",
          "upload_url",
          "expires_at"
        ],
        "title": "UploadInitiation",
        "type": "object"
      },
      "UsageEstimateResponse": {
        "description": "Wire-format cost estimate for hypothetical byte volumes.\n\nCosts are computed server-side from the rate card so clients never hardcode\nrates. The rate card is echoed (``*_per_gb`` + ``bytes_per_gb``) for\ntransparency and so a client can interpolate locally if it wants. A meter\nnot asked about returns ``null`` cents. ``metered`` is ``false`` for BYOSA\norgs, whose per-MB meters are waived \u2014 all requested costs are then ``0``.\nAll money is integer cents; byte counts are raw bytes.",
        "properties": {
          "bytes_per_gb": {
            "title": "Bytes Per Gb",
            "type": "integer"
          },
          "download_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Download Bytes"
          },
          "download_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Download Cents"
          },
          "download_cents_per_gb": {
            "title": "Download Cents Per Gb",
            "type": "integer"
          },
          "metered": {
            "title": "Metered",
            "type": "boolean"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "storage_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Storage Bytes"
          },
          "storage_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Storage Cents"
          },
          "storage_cents_per_gb_month": {
            "title": "Storage Cents Per Gb Month",
            "type": "integer"
          },
          "upload_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Upload Bytes"
          },
          "upload_cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Upload Cents"
          },
          "upload_cents_per_gb": {
            "title": "Upload Cents Per Gb",
            "type": "integer"
          }
        },
        "required": [
          "organization_id",
          "metered",
          "upload_bytes",
          "upload_cents",
          "download_bytes",
          "download_cents",
          "storage_bytes",
          "storage_cents",
          "upload_cents_per_gb",
          "download_cents_per_gb",
          "storage_cents_per_gb_month",
          "bytes_per_gb"
        ],
        "title": "UsageEstimateResponse",
        "type": "object"
      },
      "UsageEventPage": {
        "description": "Paginated response for the usage-event listing.\n\nBidirectional cursor pagination: ``items`` is the page (always newest first).\n``next_cursor`` walks toward older rows (pass it back as ``cursor=...`` with\n``direction=next``); ``prev_cursor`` walks toward newer rows (pass it back\nwith ``direction=prev``). Either is null when there is no page in that\ndirection: ``next_cursor`` is null on the oldest page, ``prev_cursor`` is null\non the newest (first) page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/UsageEventResponse"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          },
          "prev_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Prev Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "UsageEventPage",
        "type": "object"
      },
      "UsageEventResponse": {
        "description": "Wire shape for one usage-event ledger row.\n\nByte counts are raw bytes; ``cents`` is the integer-cent derived cost of the\nevent (null for ``storage`` rows, whose cost is a period integral, and for an\nunmetered org it is ``0``). ``project_id`` / ``build_id`` are null on org-level\nrollups or after the referenced row was hard-deleted.\n\n``version_name`` / ``build_number`` / ``project_name`` carry the human\nartifact identity so the row is self-describing \u2014 the client renders\n``version_name #build_number`` instead of resolving the UUIDs.\n``original_filename`` is the name the artifact was uploaded as. Each is null\non org-level rollups and after the referenced build/project was\nhard-deleted, mirroring the null contract on ``build_id`` / ``project_id``.\n\n``transfer_failed`` is ``True`` for a download whose bytes egressed but whose\ntransfer did not complete (a tester cancelled / disconnected mid-stream). The\nbytes are still billed (Azure charged us the egress), but the client should\nrender the row as a failed transfer rather than a clean download.\n\n``below_cent`` is ``True`` when the true cost was a positive sub-cent fraction\nthat ``cents`` rounded up to 1\u00a2; the client renders \"< $0.01\" for those\nrows instead of \"$0.01\". It is always paired with ``cents == 1`` and is\n``false`` for zero-cost, storage, and unmetered rows.\n\n``client_subnet`` is the anonymised source IP of the requester,\nCIDR-truncated to a /24 (IPv4) or /40 (IPv6) so the final octet is zeroed; the\nraw IP is never stored. ``null`` on rows with no captured requester (storage /\nperiod-close rows and events predating IP capture); the client renders a dash.\n\n``exact_nanocents`` is the exact, un-rounded per-event cost as an integer in\nunits of 1e-9 cents: the same ``bytes \u00d7 rate \u00f7 bytes-per-GB`` as\n``cents`` but WITHOUT the round-up to the whole cent, carried losslessly (an\ninteger, never a float) so the client can display the true sub-cent amount\n(divide by 1e9 for cents, 1e11 for dollars). ``cents`` remains what is\nactually charged. Mirrors ``cents``' null contract: ``null`` for ``storage``\nrows (billed from the period integral, no per-row cost) and ``0`` for an\nunmetered (BYOSA) org.",
        "properties": {
          "below_cent": {
            "title": "Below Cent",
            "type": "boolean"
          },
          "build_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Build Id"
          },
          "build_number": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Build Number"
          },
          "bytes": {
            "title": "Bytes",
            "type": "integer"
          },
          "cents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cents"
          },
          "client_subnet": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Subnet"
          },
          "exact_nanocents": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Exact Nanocents"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "meter": {
            "enum": [
              "upload",
              "download",
              "storage"
            ],
            "title": "Meter",
            "type": "string"
          },
          "occurred_at": {
            "format": "date-time",
            "title": "Occurred At",
            "type": "string"
          },
          "original_filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Original Filename"
          },
          "project_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project Id"
          },
          "project_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Project Name"
          },
          "source_kind": {
            "title": "Source Kind",
            "type": "string"
          },
          "transfer_failed": {
            "title": "Transfer Failed",
            "type": "boolean"
          },
          "version_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Version Name"
          }
        },
        "required": [
          "identifier",
          "meter",
          "bytes",
          "occurred_at",
          "source_kind",
          "project_id",
          "build_id",
          "cents",
          "version_name",
          "build_number",
          "project_name",
          "original_filename",
          "transfer_failed",
          "below_cent",
          "client_subnet"
        ],
        "title": "UsageEventResponse",
        "type": "object"
      },
      "UsageSummaryResponse": {
        "description": "Per-organization usage snapshot for the current billing period.\n\n``storage_cents`` is the **month-to-date accrued** storage cost \u2014 the\ntime-weighted total so far, not a projection. It grows through the month and\nreads near zero right after a large upload, because the new bytes have been\nheld for only a sliver of the period so far. Label it accordingly: it\ndeliberately diverges from ``current_storage_bytes``, which is a\npoint-in-time snapshot of what you are storing now.\n\n``upload_cents`` and ``download_cents`` are full period totals. All money is\ninteger cents; byte counts are raw bytes.",
        "properties": {
          "current_storage_bytes": {
            "title": "Current Storage Bytes",
            "type": "integer"
          },
          "download_bytes": {
            "title": "Download Bytes",
            "type": "integer"
          },
          "download_cents": {
            "title": "Download Cents",
            "type": "integer"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "period_end": {
            "format": "date-time",
            "title": "Period End",
            "type": "string"
          },
          "period_start": {
            "format": "date-time",
            "title": "Period Start",
            "type": "string"
          },
          "prepaid_balance_cents": {
            "title": "Prepaid Balance Cents",
            "type": "integer"
          },
          "prepaid_enabled": {
            "title": "Prepaid Enabled",
            "type": "boolean"
          },
          "storage_cents": {
            "title": "Storage Cents",
            "type": "integer"
          },
          "total_cents": {
            "title": "Total Cents",
            "type": "integer"
          },
          "upload_bytes": {
            "title": "Upload Bytes",
            "type": "integer"
          },
          "upload_cents": {
            "title": "Upload Cents",
            "type": "integer"
          }
        },
        "required": [
          "organization_id",
          "period_start",
          "period_end",
          "upload_bytes",
          "upload_cents",
          "download_bytes",
          "download_cents",
          "current_storage_bytes",
          "storage_cents",
          "total_cents",
          "prepaid_balance_cents",
          "prepaid_enabled"
        ],
        "title": "UsageSummaryResponse",
        "type": "object"
      },
      "ValidationErrorItem": {
        "additionalProperties": false,
        "description": "One field-level failure. Carries only these three keys: the submitted value is never echoed back.",
        "properties": {
          "loc": {
            "description": "Path to the offending field, for example `[\"body\", \"email\"]`.",
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "description": "Human-readable explanation of this field's failure.",
            "type": "string"
          },
          "type": {
            "description": "Machine-readable failure kind, for example `missing` or `string_too_short`.",
            "type": "string"
          }
        },
        "required": [
          "type",
          "loc",
          "msg"
        ],
        "title": "ValidationErrorItem",
        "type": "object"
      },
      "ValidationErrorResponse": {
        "description": "The body returned when a request fails validation. It is the same flat envelope as `ErrorResponse` with `error` fixed to `validation_error` and `details.errors` listing one entry per field failure. `message` repeats the first entry's `msg` so a caller with nowhere to render the list still has something to show.",
        "properties": {
          "details": {
            "properties": {
              "errors": {
                "description": "One entry per field that failed validation.",
                "items": {
                  "$ref": "#/components/schemas/ValidationErrorItem"
                },
                "type": "array"
              }
            },
            "required": [
              "errors"
            ],
            "type": "object"
          },
          "error": {
            "const": "validation_error",
            "description": "Always `validation_error`.",
            "type": "string"
          },
          "message": {
            "description": "The first field failure's message, safe to surface in a user interface.",
            "type": "string"
          }
        },
        "required": [
          "error",
          "message",
          "details"
        ],
        "title": "ValidationErrorResponse",
        "type": "object"
      },
      "VersionResponse": {
        "description": "The running build's identifier and environment.",
        "properties": {
          "build": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Build"
          },
          "environment": {
            "title": "Environment",
            "type": "string"
          }
        },
        "required": [
          "build",
          "environment"
        ],
        "title": "VersionResponse",
        "type": "object"
      },
      "WebhookDelivery": {
        "description": "A delivery attempt record, for the observability list endpoint.\n\nThe signed body isn't echoed \u2014 only the event type, the delivery outcome,\nand retry bookkeeping, so an operator can see whether a receiver is healthy\nwithout the endpoint becoming a way to re-read payloads.",
        "properties": {
          "attempt_count": {
            "title": "Attempt Count",
            "type": "integer"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "dead_lettered": {
            "title": "Dead Lettered",
            "type": "boolean"
          },
          "delivered": {
            "title": "Delivered",
            "type": "boolean"
          },
          "event_type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "last_attempt_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Attempt At"
          },
          "last_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Error"
          },
          "last_status_code": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Status Code"
          },
          "subscription_id": {
            "anyOf": [
              {
                "format": "uuid",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Subscription Id"
          }
        },
        "required": [
          "identifier",
          "event_type",
          "attempt_count",
          "delivered",
          "dead_lettered",
          "created_at"
        ],
        "title": "WebhookDelivery",
        "type": "object"
      },
      "WebhookDeliveryPage": {
        "description": "Cursor-paginated page of delivery records.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "WebhookDeliveryPage",
        "type": "object"
      },
      "WebhookEventType": {
        "description": "The catalogue of event types a subscription can select.\n\nValues are stable strings in ``resource.action`` form and are part of the\ncontract: new event types may be added, existing ones are never renamed.\n\nAll five resource events are delivered:\n\n* ``release.created`` \u2014 a build is published into a channel, including a\n  release created by auto-distribution.\n* ``release.deleted`` \u2014 a release is removed from a channel. Only the real\n  transition fires; deleting an already-deleted release does not fire again.\n* ``release.rolled_back`` \u2014 a channel is rolled back to a previous release.\n* ``build.uploaded`` \u2014 a build upload completes, whether or not the build\n  lands on an auto-distribute channel.\n* ``build.deleted`` \u2014 a build is deleted.\n\n``webhook.test`` is delivery-only and cannot be subscribed to: a subscription\nthat lists it is rejected. It is delivered only when a test is triggered on one\nsubscription, which sends exactly one synthetic delivery to that subscription.\n\nDelivery is best-effort and never blocks the action that raised it: if an event\ncannot be enqueued it is simply not delivered, and the originating request\nstill succeeds.",
        "enum": [
          "release.created",
          "release.deleted",
          "release.rolled_back",
          "build.uploaded",
          "build.deleted",
          "webhook.test"
        ],
        "title": "WebhookEventType",
        "type": "string"
      },
      "WebhookSubscription": {
        "description": "A subscription as returned by the read routes.\n\nCarries NO signing secret \u2014 only the public configuration + lifecycle\nmetadata. The secret is shown exactly once, on create.",
        "properties": {
          "active": {
            "title": "Active",
            "type": "boolean"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "event_types": {
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "title": "Event Types",
            "type": "array"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "updated_at": {
            "format": "date-time",
            "title": "Updated At",
            "type": "string"
          },
          "url": {
            "title": "Url",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "url",
          "event_types",
          "active",
          "created_at",
          "updated_at"
        ],
        "title": "WebhookSubscription",
        "type": "object"
      },
      "WebhookSubscriptionCreated": {
        "description": "The create response: a subscription plus the one-time plaintext signing\nsecret. The secret is never retrievable again \u2014 the receiver must store it\nnow to verify the ``X-AppGantry-Signature`` header on deliveries.",
        "properties": {
          "active": {
            "title": "Active",
            "type": "boolean"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "event_types": {
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "title": "Event Types",
            "type": "array"
          },
          "identifier": {
            "format": "uuid",
            "title": "Identifier",
            "type": "string"
          },
          "organization_id": {
            "format": "uuid",
            "title": "Organization Id",
            "type": "string"
          },
          "secret": {
            "title": "Secret",
            "type": "string"
          },
          "updated_at": {
            "format": "date-time",
            "title": "Updated At",
            "type": "string"
          },
          "url": {
            "title": "Url",
            "type": "string"
          }
        },
        "required": [
          "identifier",
          "organization_id",
          "url",
          "event_types",
          "active",
          "created_at",
          "updated_at",
          "secret"
        ],
        "title": "WebhookSubscriptionCreated",
        "type": "object"
      },
      "WebhookSubscriptionPage": {
        "description": "Cursor-paginated page of subscriptions. ``next_cursor`` is the opaque\ntoken to pass back as ``cursor=...``; null means this was the last page.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/WebhookSubscription"
            },
            "title": "Items",
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor"
          }
        },
        "required": [
          "items"
        ],
        "title": "WebhookSubscriptionPage",
        "type": "object"
      },
      "WebhookTestSendResult": {
        "description": "202 response of ``POST .../webhooks/{id}/test``.\n\nCarries the id of the synthetic ``webhook.test`` delivery just enqueued for\nthe targeted subscription, so the caller can poll the delivery list\n(``GET .../webhooks/deliveries``) for its outcome \u2014 exactly like a\nfan-out-produced delivery, but scoped to one subscription on demand.",
        "properties": {
          "delivery_id": {
            "format": "uuid",
            "title": "Delivery Id",
            "type": "string"
          }
        },
        "required": [
          "delivery_id"
        ],
        "title": "WebhookTestSendResult",
        "type": "object"
      },
      "_ResendVerificationRequest": {
        "description": "Body for POST /developers/resend-verification.",
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "_ResendVerificationRequest",
        "type": "object"
      }
    },
    "securitySchemes": {
      "AppGantryAuthBase": {
        "description": "Send the credential as `Authorization: Bearer <token>`. Three kinds are accepted:\n\n- **Developer JWT** \u2014 the short-lived access token returned by `POST /api/v1/login/email` (or by the SSO exchange). It carries the developer's own capability, bounded by their organization and project roles, and expires in minutes.\n- **Personal Access Token** (`ag_pat_\u2026`) \u2014 a long-lived integration credential bound to a single organization and capped by the scopes it was issued with. It can never exceed its owner's roles, and never acts outside the organization it was minted for.\n- **Project access token** (`ag_prj_\u2026`) \u2014 bound to one project, for automation that should not carry a person's identity.\n\nBrowser clients may instead rely on the `dfdeveloper` cookie that sign-in sets. Its value is the same developer JWT and the server validates it identically; it is only read when the request carries no `Authorization` header, so a bearer token always wins.\n\nSome routes are **JWT-only**: they refuse Personal Access Tokens and project access tokens with 403 regardless of scope. Those operations carry `x-appgantry-auth: jwt-only` and say so in their description. They are the surfaces where a long-lived machine credential would be the wrong trust level \u2014 credential management, account and session management, SSO and storage-account configuration, organization creation, and the per-developer audit feed.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "API for mobile app distribution across Apple and Android platforms\n\n## Authentication\n\nMost operations need a developer credential, sent as a bearer token in the `Authorization: Bearer <token>` header. The security scheme below lists the credential types and which operations accept only an interactive sign-in.\n\nThree surfaces need no developer credential at all: signing in, the public install surface under `/public/v1`, and the in-app update surface under `/sdk/v1`. `/sdk/v1` is not unauthenticated, though: most of its operations carry the per-install **update token** that enrolment mints, in the same `Authorization: Bearer <token>` header. Two are exceptions because the caller has no token yet or cannot send a header at all - enrolment exchanges the app key and app secret for the update token, and the iOS install manifest is fetched by the operating system with a short-lived signed capability token in the query string.\n\nTo try requests here, use a **Personal Access Token (PAT)**: sign in to your AppGantry dashboard, create one under [Settings > Personal access tokens](https://app.appgantrystaging.com/settings/personal-access-tokens/new), then paste it into the **Authentication** panel.",
    "title": "AppGantry App Distribution",
    "version": "v1"
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/v1/": {
      "get": {
        "description": "Main entry point. Only returns a link to documentation.",
        "operationId": "root_api_v1__get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          }
        },
        "summary": "Root",
        "tags": [
          "Miscellaneous"
        ]
      }
    },
    "/api/v1/audit/developer/{developer_id}": {
      "get": {
        "description": "Return per-developer events (signup, login, refresh, logout, PAT\nlifecycle, email verification) for the named developer.\n\n**Authentication: JWT only.** A Personal Access Token or project access token\nis rejected with 403 even when its scope would otherwise allow a read. A PAT\nis bound to one organization, while this feed spans the whole developer\n(logins, PAT lifecycle across every org they belong to, email verifications)\n\u2014 serving it to a PAT scoped to org A would expose the owner's activity in\norgs B and C.\n\n**Self-only.** The developer id in the URL must be the caller's own;\nany other developer's feed answers 401.\n\n**Two query modes:**\n\n* *Per-day* (the default): ``?date=YYYY-MM-DD`` selects one UTC day.\n  Omitting it means today (UTC). Returns at most 1000 events.\n* *Range export*: ``?from=YYYY-MM-DD&to=YYYY-MM-DD`` returns every event in\n  the inclusive UTC range. Either bound defaults to today, and ``from`` must\n  not be after ``to``. Returns at most 5000 events.\n\nSupplying either ``from`` or ``to`` selects the range mode and ``date`` is\nignored. Events come back newest-first. When a query hits its cap the trail\nis incomplete and the response carries ``X-Audit-Truncated: true``; narrow\nthe range to see the rest.\n\nResponse bodies are unredacted \u2014 a developer's own feed holds nothing that is\nhidden from them. (The per-ORGANIZATION feed does redact; see that route.)",
        "operationId": "get_developer_logs_api_v1_audit_developer__developer_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "date",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Date"
            }
          },
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "From"
            }
          },
          {
            "in": "query",
            "name": "to",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "To"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "additionalProperties": true,
                    "description": "One audit event. Every event carries the base fields listed here; each event type adds its own fields on top (for example `build_id` on build events, `container_name` on storage-backend events), so this object is intentionally open-ended.",
                    "properties": {
                      "data_type": {
                        "description": "Stable numeric type of the resource the event is about.",
                        "type": "integer"
                      },
                      "editor_id": {
                        "description": "Who performed the action. A developer id for self-service and operator actions; the platform-operator sentinel id for system, background and webhook actions.",
                        "format": "uuid",
                        "type": "string"
                      },
                      "editor_kind": {
                        "description": "What `editor_id` points at: a developer, or a project access token.",
                        "type": "integer"
                      },
                      "event_type": {
                        "description": "Stable numeric event type. Values are never renumbered or repurposed, so they are safe to switch on.",
                        "type": "integer"
                      },
                      "organization_id": {
                        "description": "The owning organization. All-zeros on developer-partition events that happened outside any organization (signup, login, PAT lifecycle).",
                        "format": "uuid",
                        "type": "string"
                      },
                      "timestamp": {
                        "description": "When the action happened, ISO-8601 UTC with a `Z` suffix.",
                        "format": "date-time",
                        "type": "string"
                      }
                    },
                    "required": [
                      "timestamp",
                      "event_type",
                      "editor_id",
                      "organization_id"
                    ],
                    "title": "AuditEvent",
                    "type": "object"
                  },
                  "title": "Response Get Developer Logs Api V1 Audit Developer  Developer Id  Get",
                  "type": "array"
                }
              }
            },
            "description": "The developer's audit events for the requested day or range, newest first.",
            "headers": {
              "X-Audit-Truncated": {
                "description": "Present and set to `true` when the query hit its result cap, meaning the returned trail is incomplete. The caps are 1000 events for a single-day query and 5000 events for a date-range export. Narrow the range (or query day by day) to see the rest. Absent when the result is complete.",
                "schema": {
                  "enum": [
                    "true"
                  ],
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The audit backend rejected the query and no trail could be read."
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The audit backend did not answer within the read timeout. Retry after the delay in `Retry-After`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get a developer's audit log (per-day or over a date range)",
        "tags": [
          "Audit"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/audit/organization/{organization_id}": {
      "get": {
        "description": "Return audit events in this organization's partition.\n\n**Authentication: JWT or PAT.** A PAT needs `READ` scope and must be bound to\nthis organization. The caller must hold the organization `ADMIN` role: this\ncross-cutting feed exposes per-tester actions and per-project events from\nevery project in the org \u2014 a strictly broader view than any single project\nowner has \u2014 so it is restricted to the principal who already sees everything.\n\n**Two query modes:**\n\n* *Per-day* (the default): ``?date=YYYY-MM-DD`` selects one UTC day.\n  Omitting it means today (UTC). Returns at most 1000 events.\n* *Range export*: ``?from=YYYY-MM-DD&to=YYYY-MM-DD`` returns every event in\n  the inclusive UTC range. Either bound defaults to today, and ``from`` must\n  not be after ``to``. Returns at most 5000 events.\n\nSupplying either ``from`` or ``to`` selects the range mode and ``date`` is\nignored. Events come back newest-first. When a query hits its cap the trail\nis incomplete and the response carries ``X-Audit-Truncated: true``; narrow\nthe range (or walk it day by day) to see the rest.\n\n**What is included.** Organization, project, build, channel, release, tester\nand tester-membership events all share this organization's partition and come\nback together. Per-developer events (signup, login, PAT lifecycle) live in a\nseparate per-developer partition and are NOT included \u2014 read those from\n``GET /api/v1/audit/developer/{developer_id}``.\n\n**Redaction.** Operator-sensitive values are masked on this feed in both query\nmodes: the storage tenant id and account URL on storage-backend events (the\ncontainer name is left intact \u2014 the customer chose it), and the invitee email\non invite / tester-grant / channel-invite events, which is masked to\n``****@domain`` so an admin cannot harvest every address ever invited.",
        "operationId": "get_organization_logs_api_v1_audit_organization__organization_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "date",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Date"
            }
          },
          {
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "From"
            }
          },
          {
            "in": "query",
            "name": "to",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "To"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "additionalProperties": true,
                    "description": "One audit event. Every event carries the base fields listed here; each event type adds its own fields on top (for example `build_id` on build events, `container_name` on storage-backend events), so this object is intentionally open-ended.",
                    "properties": {
                      "data_type": {
                        "description": "Stable numeric type of the resource the event is about.",
                        "type": "integer"
                      },
                      "editor_id": {
                        "description": "Who performed the action. A developer id for self-service and operator actions; the platform-operator sentinel id for system, background and webhook actions.",
                        "format": "uuid",
                        "type": "string"
                      },
                      "editor_kind": {
                        "description": "What `editor_id` points at: a developer, or a project access token.",
                        "type": "integer"
                      },
                      "event_type": {
                        "description": "Stable numeric event type. Values are never renumbered or repurposed, so they are safe to switch on.",
                        "type": "integer"
                      },
                      "organization_id": {
                        "description": "The owning organization. All-zeros on developer-partition events that happened outside any organization (signup, login, PAT lifecycle).",
                        "format": "uuid",
                        "type": "string"
                      },
                      "timestamp": {
                        "description": "When the action happened, ISO-8601 UTC with a `Z` suffix.",
                        "format": "date-time",
                        "type": "string"
                      }
                    },
                    "required": [
                      "timestamp",
                      "event_type",
                      "editor_id",
                      "organization_id"
                    ],
                    "title": "AuditEvent",
                    "type": "object"
                  },
                  "title": "Response Get Organization Logs Api V1 Audit Organization  Organization Id  Get",
                  "type": "array"
                }
              }
            },
            "description": "The organization's audit events for the requested day or range, newest first, with operator-sensitive fields redacted.",
            "headers": {
              "X-Audit-Truncated": {
                "description": "Present and set to `true` when the query hit its result cap, meaning the returned trail is incomplete. The caps are 1000 events for a single-day query and 5000 events for a date-range export. Narrow the range (or query day by day) to see the rest. Absent when the result is complete.",
                "schema": {
                  "enum": [
                    "true"
                  ],
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The audit backend rejected the query and no trail could be read."
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The audit backend did not answer within the read timeout. Retry after the delay in `Retry-After`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get an organization's audit log (per-day or over a date range)",
        "tags": [
          "Audit"
        ]
      }
    },
    "/api/v1/builds": {
      "get": {
        "description": "List builds for a project, newest first, cursor-paginated.\n\nPagination is keyset/cursor based: callers pass ``limit`` and\noptionally a ``cursor`` token from the previous page. The cursor\nis opaque: treat it as a black box and pass back exactly the token the\nprevious response returned.\n\nThe default page size is 100, hard-capped at 1000.\n\nOptional server-side filters, all AND-combined and keyset-safe\n(each narrows the set without reordering it, so the cursor stays correct\nunder any combination):\n\n* ``platform`` - single platform.\n* ``status`` - ``active`` (default), ``revoked``, or ``all``. Takes\n  precedence over the legacy ``include_revoked`` flag; when ``status`` is\n  omitted, ``include_revoked=false`` means active-only and ``true`` means\n  all, preserving the previous contract.\n* ``uploaded_after`` / ``uploaded_before`` - ISO 8601 instants. The window\n  is ``uploaded_after <= uploaded_at < uploaded_before`` (after inclusive,\n  before exclusive). Naive values are interpreted as UTC.\n* ``q`` - free-text search: case-insensitive substring on ``version_name``,\n  additionally matching ``build_number`` exactly when ``q`` is a plain\n  integer. Blank/whitespace ``q`` is a no-op.",
        "operationId": "get_builds_api_v1_builds_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "platform",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Platform"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Platform"
            }
          },
          {
            "in": "query",
            "name": "include_revoked",
            "required": false,
            "schema": {
              "default": false,
              "title": "Include Revoked",
              "type": "boolean"
            }
          },
          {
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/BuildStatusFilter"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Status"
            }
          },
          {
            "in": "query",
            "name": "uploaded_after",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "format": "date-time",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Uploaded After"
            }
          },
          {
            "in": "query",
            "name": "uploaded_before",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "format": "date-time",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Uploaded Before"
            }
          },
          {
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "maxLength": 64,
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Q"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Builds",
        "tags": [
          "Builds"
        ]
      }
    },
    "/api/v1/builds/initiate": {
      "post": {
        "description": "Commit a pending upload row and return a signed PUT URL.\n\nThe caller then PUTs the artifact bytes directly to ``upload_url``, which\naddresses object storage rather than this API and therefore carries no\nAppGantry ``Authorization`` header. When the upload is complete the caller\nposts to ``POST /builds/{pending_upload_id}/complete``, which verifies the\nstored object's size, records the client-supplied SHA-256 as the build's\nintegrity fingerprint, and promotes the pending row into a build.\n\nThe signed URL and the pending row both expire after 1 hour. A pending row\nwhose ``expires_at`` has passed is reclaimed along with any orphan blob.",
        "operationId": "initiate_upload_api_v1_builds_initiate_post",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewBuild"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadInitiation"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. The organization's subscription payment failed, its monthly spend cap would be exceeded by this upload, or its pre-paid credit is exhausted. Existing builds and downloads are unaffected \u2014 only new uploads are refused."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Storage backend lookup failed transiently. Retry after the delay in `Retry-After`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Initiate Upload",
        "tags": [
          "Builds"
        ]
      }
    },
    "/api/v1/builds/{build_id}": {
      "delete": {
        "description": "Hard-delete a build row. Storage cleanup is separate.\n\nADMIN-only because it destroys audit-trail-adjacent state. Soft-delete\nvia /{build_id}/revoke is the usual path; this is for emergencies\n(build with secrets accidentally uploaded, etc.).",
        "operationId": "delete_build_api_v1_builds__build_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Build",
        "tags": [
          "Builds"
        ]
      },
      "get": {
        "description": "Get a single build, including revoked ones.",
        "operationId": "get_build_api_v1_builds__build_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Build",
        "tags": [
          "Builds"
        ]
      },
      "patch": {
        "description": "Correct a build's release notes after upload.\n\nOnly ``release_notes`` is editable; the artifact, its SHA-256, and every\nother field are immutable from initiate time onward. It takes the same\nupload authority as initiate/complete rather than the delete/admin\nauthority: fixing a typo in the notes is an upload-adjacent correction,\nnot a destructive op.\n\nEditing an already-revoked build is allowed (revoke is a soft-delete that\nleaves the record intact). Submitting the value already stored is a no-op\n\u2014 the build is returned unchanged and no audit event is emitted.\nOmitting ``release_notes`` from the body entirely is likewise a no-op\n(PATCH semantics: an absent field is untouched, NOT cleared); only an\nexplicit ``null`` clears the notes. NOT_FOUND if the build is unknown or\nbelongs to a different tenant (uniform shape, no enumeration channel).",
        "operationId": "update_build_api_v1_builds__build_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateBuildReleaseNotes"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Build",
        "tags": [
          "Builds"
        ]
      }
    },
    "/api/v1/builds/{build_id}/download": {
      "get": {
        "description": "Hand out a download URL for a build, recording the request first.\n\nThe request is authenticated and authorised, the build is checked against the\nproject and organization the caller named, the download is recorded against\nthe calling developer for analytics, and a short-lived signed URL is returned\nas JSON (``download_url``). The caller issues the GET against that URL itself\n\u2014 the response is data, not a redirect.\n\nTester-side downloads (invite-token authentication, an install link in an\nemail) live under the testers surface and have their own route, not this one.",
        "operationId": "download_build_api_v1_builds__build_id__download_get",
        "parameters": [
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildDownloadResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Downloads are suspended after the grace window on an organization whose subscription payment failed, or the signed URL would push the organization past its monthly spend cap or exhaust its pre-paid credit."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Storage backend lookup failed transiently. Retry after the delay in `Retry-After`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Download Build",
        "tags": [
          "Builds"
        ]
      }
    },
    "/api/v1/builds/{build_id}/revoke": {
      "post": {
        "description": "Soft-delete a build: hide from testers but keep the row and artifact.",
        "operationId": "revoke_build_api_v1_builds__build_id__revoke_post",
        "parameters": [
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke Build",
        "tags": [
          "Builds"
        ]
      }
    },
    "/api/v1/builds/{pending_upload_id}/complete": {
      "post": {
        "description": "Finalise a direct upload and promote the pending row to a build.\n\nThe client reports the SHA-256 it computed while streaming the\nbytes to the signed URL (``completion.sha256``); the server stores\nit as the build's integrity fingerprint without re-downloading the\nartifact. Size IS verified server-side via a metadata-only HEAD on\nthe blob and checked against the upload cap, so a client cannot\nsneak an oversize artifact past the limit even though the bytes\nnever flow back through the API. See ``UploadCompletion`` for the\nfull trust-model rationale.\n\nNOT_FOUND if the pending upload ID is unknown, has expired, or\nbelongs to a different tenant \u2014 same shape for all three so\ncross-tenant probes can't enumerate. 413 if the stored blob\nexceeds the upload cap (the server deletes the blob + pending\nrow before returning).",
        "operationId": "complete_upload_api_v1_builds__pending_upload_id__complete_post",
        "parameters": [
          {
            "in": "path",
            "name": "pending_upload_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Upload Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadCompletion"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Complete Upload",
        "tags": [
          "Builds"
        ]
      }
    },
    "/api/v1/captcha": {
      "get": {
        "description": "Return the captcha page.\n\nThe hcaptcha JS is loaded from a third-party CDN. hcaptcha doesn't\ncurrently publish SRI hashes for api.js (the bundle is mutated for\nA/B / regional variants), so the integrity attribute is omitted\ndeliberately rather than pinning a value that would silently break\non the next hcaptcha release. The CSP is the real defence; it\npins script + frame sources to hcaptcha origins.",
        "operationId": "captcha_api_v1_captcha_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          }
        },
        "summary": "Captcha",
        "tags": [
          "Miscellaneous"
        ]
      }
    },
    "/api/v1/channels": {
      "get": {
        "description": "List channels for a project, newest first, cursor-paginated.\n\nPass ``limit`` and optionally a ``cursor`` token from the previous page's\n``next_cursor``. Was a bare array silently truncated at 1000; now the\nfull set is reachable page by page.",
        "operationId": "get_channels_api_v1_channels_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Channels",
        "tags": [
          "Channels"
        ]
      },
      "post": {
        "description": "Create a channel in a project.",
        "operationId": "create_channel_api_v1_channels_post",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewChannel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Channel",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/channels/{channel_id}": {
      "delete": {
        "description": "Delete a channel. Cascades to its releases and tester memberships.",
        "operationId": "delete_channel_api_v1_channels__channel_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Channel",
        "tags": [
          "Channels"
        ]
      },
      "get": {
        "description": "Get a single channel.",
        "operationId": "get_channel_api_v1_channels__channel_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Channel",
        "tags": [
          "Channels"
        ]
      },
      "patch": {
        "description": "Update a channel's name and/or description.",
        "operationId": "update_channel_api_v1_channels__channel_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchChannel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Channel",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/channels/{channel_id}/analytics": {
      "get": {
        "description": "Aggregate install counters for a whole channel / distribution group.\n\nTotal downloads, unique testers, distinct released builds, and first/last\ndownload timestamps summed across every build released on the channel. No\ntester PII (use ``/releases/{id}/install-status`` for per-tester detail), so\nREAD access suffices.",
        "operationId": "get_channel_analytics_api_v1_channels__channel_id__analytics_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAnalytics"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Install/download analytics for a channel (distribution group)",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/channels/{channel_id}/tester-invites": {
      "get": {
        "description": "List non-expired, unaccepted invites for the channel, newest first,\ncursor-paginated. The listing strips the raw invite token.",
        "operationId": "list_channel_invites_api_v1_channels__channel_id__tester_invites_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelInvitePage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List pending channel tester invites",
        "tags": [
          "Channels"
        ]
      },
      "post": {
        "description": "Mint a single-use invite token (10-minute TTL).\n\nBody is optional. When supplied (with an email), the server sends\nthe invite email. When omitted, the caller distributes the token\nthemselves (CLI, admin UI, etc.).",
        "operationId": "create_channel_invite_api_v1_channels__channel_id__tester_invites_post",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/AddTesterRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create a 10-minute channel tester invite link",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/channels/{channel_id}/tester-invites/{invite_id}": {
      "delete": {
        "description": "Revoke a pending invite \u2014 hard-delete since the token is the only artifact.",
        "operationId": "revoke_channel_invite_api_v1_channels__channel_id__tester_invites__invite_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke a pending channel tester invite",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/channels/{channel_id}/testers": {
      "get": {
        "description": "List the direct channel-tier tester grants, newest first, cursor-paginated. Inherited grants (org-tier / project-tier) are intentionally not\nincluded. Was a bare array truncated at 1000.",
        "operationId": "list_channel_testers_api_v1_channels__channel_id__testers_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List direct channel tester grants",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/channels/{channel_id}/testers/{developer_id}": {
      "delete": {
        "description": "Remove a direct channel tester grant. Doesn't affect inherited grants.",
        "operationId": "remove_channel_tester_api_v1_channels__channel_id__testers__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove a direct channel tester grant",
        "tags": [
          "Channels"
        ]
      }
    },
    "/api/v1/developers": {
      "post": {
        "description": "Create a new developer account and dispatch a verification email.\n\nThe verification email is sent on a background task (after the response),\nnot inline, so a new signup and a duplicate-suppressed signup return in the\nsame time: an attacker can't use response latency to tell whether an email\nis already registered. The background task is scheduled\nunconditionally and keyed on the returned id, so the duplicate path (which\nyields a phantom id with no row) sends nothing \u2014 preserving the existing\n\"no email to an existing account's mailbox on duplicate signup\" contract.\nEmail failures are silently swallowed; the user can re-request via\n/resend-verification.",
        "operationId": "create_developer_api_v1_developers_post",
        "parameters": [
          {
            "in": "header",
            "name": "x-captcha-key",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "X-Captcha-Key"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewDeveloper"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "title": "Response Create Developer Api V1 Developers Post",
                  "type": "string"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Create Developer",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/cancel_deletion": {
      "post": {
        "description": "Cancel a pending account deletion and reactivate the account.\n\nPublic (no JWT): the owner is locked out of auth while their account is\npending deletion, so the emailed single-use token is their only credential.\nReturns 204 on success. 400 on an invalid / expired / already-used token \u2014\na uniform shape so a caller can't probe token state. After cancellation the\nowner logs in normally (their pre-request sessions and PATs stay revoked\nfrom the lockout, so they re-authenticate fresh).",
        "operationId": "cancel_account_deletion_api_v1_developers_cancel_deletion_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountDeletionCancelRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Cancel Account Deletion",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/resend-verification": {
      "post": {
        "description": "Resend a verification email if there's an unverified developer with this address.\n\nAlways 204, regardless of whether the email matches a developer or\nwhether the email actually got sent. Status-code parity prevents the\nendpoint from being used to enumerate registered emails or to probe\nfor transient email-provider failures.\n\nThe lookup + token mint + email send all run on a background task (after\nthe response), so the found and not-found branches return in the same time\ntoo: response latency can't be used as an account-existence oracle. Rate-limited at Azure Front Door \u2014 see deploy/bicep/modules/edge.bicep\nfor the policy. No in-process throttle.",
        "operationId": "resend_verification_email_api_v1_developers_resend_verification_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/_ResendVerificationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Resend Verification Email",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/reset_password": {
      "post": {
        "description": "Consume a reset token and set a new password.\n\nReturns 204 on success. 400 on invalid/expired token OR malformed\nnew password \u2014 same response shape either way so an attacker can't\ndistinguish \"token was bad\" from \"I sent a password that's too\nshort\" by reading the status code (the body carries the distinction\nfor legitimate clients).\n\nThe same-shape guarantee holds for a malformed password too: a\nrequest-validation failure is reported as ``VALIDATION_ERROR`` with status\n400 rather than a 422, so it collapses onto the same 400 a bad token gets.",
        "operationId": "reset_password_api_v1_developers_reset_password_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordResetConfirm"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Reset Password",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/reset_password_request": {
      "post": {
        "description": "Issue a password reset email if there's a verified developer with this address.\n\nAlways 204, regardless of whether the email matches a developer,\nwhether they're verified, or whether the email actually got sent.\nAnti-enumeration parity with /resend-verification: an attacker\ncannot probe which emails are registered or verified by inspecting\nthe response.\n\nThe lookup, token mint, and email send run in the background (after the\nresponse) so the no-op branches and the send branch return in the same\ntime, closing the response-latency account-existence oracle.",
        "operationId": "request_password_reset_api_v1_developers_reset_password_request_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordResetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Request Password Reset",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/{developer_id}": {
      "delete": {
        "description": "Request deferred deletion of a developer account. Self-only.\n\nNo longer an immediate erasure: this schedules the account for deletion\nafter a grace window, locks it out of auth, and emails a single-use\ncancellation link. Returns 202 Accepted \u2014 the account still exists until\nthe scheduled deletion runs. The owner\ncan cancel within the window via ``POST /developers/cancel_deletion``.",
        "operationId": "delete_developer_api_v1_developers__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Developer",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "get": {
        "description": "Get developer information.",
        "operationId": "get_developer_api_v1_developers__developer_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Developer"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Developer",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "patch": {
        "description": "Update developer information. Self-only.",
        "operationId": "update_developer_api_v1_developers__developer_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchDeveloper"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Developer",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/developers/{developer_id}/avatar": {
      "delete": {
        "description": "Remove the developer's avatar. Self-only, JWT-only; idempotent 204 when none set.",
        "operationId": "delete_developer_avatar_api_v1_developers__developer_id__avatar_delete",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete the developer's avatar",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "post": {
        "description": "Store the developer's avatar.\n\nSelf-only and JWT-only: an avatar is personal profile data, so a leaked PAT\n(bound to an org, not the developer) must not be able to set it, and one\ndeveloper must not set another's. The body is capped at\na fixed size limit (enforced by reading one byte past the cap,\nindependent of any client Content-Length) and the true image type is sniffed\nfrom the bytes server-side (the declared Content-Type is ignored). 204 on\nsuccess; 400 for a non-image payload; 413 when over the size cap.",
        "operationId": "set_developer_avatar_api_v1_developers__developer_id__avatar_post",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_set_developer_avatar_api_v1_developers__developer_id__avatar_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Upload (or replace) the developer's avatar",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/developers/{developer_id}/change_email": {
      "post": {
        "description": "Request an email change (rectification). Self-only.\n\nParks the new address and mails a verification link to it; the change\nonly takes effect once the developer proves control of the new inbox by\nconsuming that token via POST /verify_email. Returns 202 because the\nchange is accepted-but-pending, never applied synchronously. The\nresponse is uniform whether or not the target address is already\nregistered, so it can't be used to enumerate accounts.\n\nRe-authentication is required before the pending change is parked:\nan email change is an account-takeover primitive (it survives session\nrevocation and a password change), so session possession alone is not\nsufficient authority. The current password is always required, plus a\nsecond factor whenever the developer has one \u2014 a valid MFA code for a\nTOTP developer, or a WebAuthn step-up assertion for a passkey-only\ndeveloper.",
        "operationId": "change_developer_email_api_v1_developers__developer_id__change_email_post",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeEmailRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Change Developer Email",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/developers/{developer_id}/change_email/step_up/passkey/begin": {
      "post": {
        "description": "Begin a passkey step-up for the sensitive-action re-auth gate.\n\nA passkey-only developer (no confirmed TOTP) must prove control of a passkey\nbefore ``change_email`` parks the pending address. This returns the\n``PublicKeyCredentialRequestOptions`` for ``navigator.credentials.get()`` and\nan opaque ``challenge_id`` the client echoes back on the change_email call as\n``passkey_challenge_id`` alongside the signed assertion.\n\nSelf-only and JWT-only, like ``change_email`` itself: a leaked PAT must not\nbe able to drive an account-takeover primitive.",
        "operationId": "begin_change_email_passkey_step_up_api_v1_developers__developer_id__change_email_step_up_passkey_begin_post",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticationBeginResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Begin Change Email Passkey Step Up",
        "tags": [
          "Developers"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/developers/{developer_id}/export": {
      "get": {
        "description": "Get the status of your most recent personal-data export. Self-only, JWT-only.\n\n404 when you've never requested one. When ``status`` is ``ready`` the\narchive is downloadable from ``GET /{developer_id}/export/{identifier}/download``\nuntil ``expires_at``.",
        "operationId": "get_personal_data_export_status_api_v1_developers__developer_id__export_get",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Personal Data Export Status",
        "tags": [
          "Developers"
        ]
      },
      "post": {
        "description": "Request a copy of your personal data (GDPR Art. 15). Self-only, JWT-only.\n\nAsynchronous: this mints (or reuses) a pending request and returns 202. A\nbackground worker assembles the archive and emails a self-only download\nlink; poll ``GET /{developer_id}/export`` for status. Repeat requests while\na job is in flight collapse onto the existing one.",
        "operationId": "request_personal_data_export_api_v1_developers__developer_id__export_post",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Request Personal Data Export",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/{developer_id}/export/{request_id}/download": {
      "get": {
        "description": "Download a ready personal-data export archive. Self-only, JWT-only.\n\nServed inline through this authenticated route rather than as a\nbearer-less signed URL, because the archive concentrates personal data. 404\nuntil the export is ready (or if it isn't yours / has expired).",
        "operationId": "download_personal_data_export_api_v1_developers__developer_id__export__request_id__download_get",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "request_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Request Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Download Personal Data Export",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/developers/{developer_id}/verify_email": {
      "get": {
        "description": "Confirmation page for the email-verification link.\n\nThe link in the verification email is a GET (every mainstream email\nclient renders <a href> as GET). If GET itself consumed the token\nwe'd be vulnerable to two real-world hazards:\n\n* Email-scanner pre-fetch (Outlook ATP Safe Links, Gmail's link\n  previewer, etc.) hits every link in an email *before* delivery\n  to look for malware. A GET-consumes-token shape lets the\n  scanner burn the developer's one-shot token, and by the time\n  they click it returns 404.\n* Browser history / address bar leakage of a still-live token\n  that an over-the-shoulder observer could replay.\n\nInstead, GET renders a minimal HTML page with a \"Verify Email\"\nbutton that POSTs the token to verify_developer_email. A scanner\nwon't auto-submit the form; the user has to click. The POST is\nwhat consumes the token. The token is HTML-escaped to defang\nanything malicious in case a future code path ever lets a non-\nsecret value reach this endpoint.",
        "operationId": "verify_developer_email_landing_api_v1_developers__developer_id__verify_email_get",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "token",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 1,
              "title": "Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Verify Developer Email Landing",
        "tags": [
          "Developers"
        ]
      },
      "post": {
        "description": "Verify a developer's email by consuming the one-shot token.\n\nPOST shape carries the token in the request body to keep it out\nof URLs (and therefore out of browser history, referrer headers,\nand proxy access logs). The GET landing page renders a button\nthat either fetch()es here as JSON (primary path) or, when JS is\nunavailable, submits the noscript <form> as\napplication/x-www-form-urlencoded. We sniff the content-type and\npull ``token`` out of whichever shape the caller sent.\n\nSuccessful verification consumes the token (one-shot) and flips\nthe developer's email_verified flag. Bad-or-expired tokens return\n404 with the same generic message regardless of which case it\nwas, so the endpoint can't be used to test which developer IDs\nexist.",
        "operationId": "verify_developer_email_api_v1_developers__developer_id__verify_email_post",
        "parameters": [
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Verify Developer Email",
        "tags": [
          "Developers"
        ]
      }
    },
    "/api/v1/devices": {
      "get": {
        "description": "List an organization's registered devices, newest first, cursor-paginated.",
        "operationId": "list_devices_api_v1_devices_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DevicePage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Devices",
        "tags": [
          "Devices"
        ]
      }
    },
    "/api/v1/devices/enroll": {
      "post": {
        "description": "Mint a device-enrollment link for an organization.\n\nThe tester opens ``enrollment_url`` in Safari on the iOS device to be\nregistered; it serves an instructional landing page that links to the\nconfiguration profile, which the device installs and then reports its UDID\nback. The link is a short-lived capability token bound to this organization.",
        "operationId": "mint_device_enrollment_api_v1_devices_enroll_post",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceEnrollment"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Mint Device Enrollment",
        "tags": [
          "Devices"
        ]
      }
    },
    "/api/v1/devices/{device_id}": {
      "delete": {
        "description": "Remove a device from the organization's registry. 404 if it doesn't exist.",
        "operationId": "remove_device_api_v1_devices__device_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove Device",
        "tags": [
          "Devices"
        ]
      }
    },
    "/api/v1/invites/accept": {
      "post": {
        "description": "Accept an invite. The token identifies the scope (org or project).\n\nThe accepter's email must match the invite \u2014 see the service\nlayer's accept_invite for the collapse-to-404 rationale.\n\nPAT-authed accepts are refused (``require_no_pat``) \u2014 accepting\nan invite is a \"this human controls this email\" assertion that\nshouldn't be deferrable to a machine token.",
        "operationId": "accept_invite_api_v1_invites_accept_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AcceptInvite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Accept an organization or project invite",
        "tags": [
          "Miscellaneous"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/invites/{invite_token}/preview": {
      "get": {
        "description": "Resolve the org/project/role names for the WebApp interstitial. No auth.\n\nThe token itself proves possession of the email link, so no bearer is\nrequired \u2014 this backs a plain GET link the recipient clicks. NOT_FOUND on\nexpired / used / unknown collapses to one shape so a stolen token can't be\nused to probe org/project names.\n\nThis is deliberately read-only: it never consumes the invite, so an email\nscanner pre-fetching the link can't burn the token. Acceptance still goes\nthrough the authenticated ``POST /invites/accept``.",
        "operationId": "preview_invite_api_v1_invites__invite_token__preview_get",
        "parameters": [
          {
            "in": "path",
            "name": "invite_token",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 1,
              "title": "Invite Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitePreview"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Preview an organization or project invite (interstitial)",
        "tags": [
          "Miscellaneous"
        ]
      }
    },
    "/api/v1/login/email": {
      "post": {
        "description": "Authenticate a developer and start a session.\n\nReturns the access JWT and refresh token in the response body; mirrors\nthem as cookies for the browser frontend (httponly + lax + secure-in-\nprod). Both fail-paths \u2014 bad credentials and unverified email \u2014 give\ndeliberate response shapes (see comments).",
        "operationId": "email_api_v1_login_email_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Email"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Log a developer in",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/logout": {
      "post": {
        "description": "Revoke the supplied refresh token; clear access + refresh cookies.\n\nAlso raises the account's minimum-issued-at cutoff so every outstanding\naccess token is rejected once the new cutoff has propagated. Propagation\ntakes a short bounded window (about 60 s), so in the worst case a token\nissued before logout can still be accepted for up to ~60 s afterward.\n\nPer-developer (not per-device) revocation is deliberate: a\nleak-with-imminent-logout scenario gets closed within that window\nrather than waiting on the access token's natural 10-minute expiry. Other\ndevices the developer is signed in on pick up a fresh access token on their\nnext refresh round-trip and keep working.\n\nBest-effort for an unknown-but-supplied token: a token that is\nunknown or already revoked still returns 204, because exposing\nwhether a *supplied* token was valid would leak signal to an\nattacker holding a guessed one.\n\nBut a caller that supplies *no* token at all is rejected.\n\"You supplied nothing\" reveals nothing about any token's validity,\nso the anti-enumeration argument does not apply \u2014 whereas silently\nreturning 204 would report a successful logout while revoking\nnothing. Non-browser clients (SDK / CLI / mobile) receive the\nrefresh token in the login JSON body rather than as a cookie; one\nthat forgets to echo it back would otherwise believe it had logged\nout while the session and its 30-day refresh token stayed fully\nlive, with no signal that revocation never happened. This mirrors\nthe sibling ``refresh`` handler, which rejects the same no-token\ncase.\n\nAn audit event is emitted only when the token was actually revoked here,\nnot on no-op replays.",
        "operationId": "logout_api_v1_login_logout_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Revoke a refresh token and clear session cookies",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/mfa": {
      "post": {
        "description": "Complete a login by redeeming the MFA challenge with a second factor.\n\n``mfa_token`` is the challenge handed back by ``/login/email`` for an\nMFA-enabled account; ``code`` is a TOTP code or a single-use recovery code. On\nsuccess this issues exactly the same session (access JWT + rotating refresh\ntoken, mirrored as cookies) as a password-only login. Every failure collapses\nto a single 401 so the endpoint reveals nothing about which challenges,\naccounts, or codes exist.",
        "operationId": "mfa_verify_api_v1_login_mfa_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MfaVerifyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Complete a login by verifying a second factor",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/mfa/passkey/begin": {
      "post": {
        "description": "Start a passkey step-up completing a password-triggered MFA challenge.\n\nA developer who has a passkey but no TOTP (or who simply prefers their\npasskey) completes the ``/login/email`` MFA challenge here instead of at\n``/login/mfa``. This returns the same ``PublicKeyCredentialRequestOptions`` as\npasswordless login (empty ``allowCredentials`` so the browser offers any\ndiscoverable passkey); the developer is not identified until ``finish`` binds\nthe asserted passkey to the challenge's account.",
        "operationId": "mfa_passkey_begin_api_v1_login_mfa_passkey_begin_post",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticationBeginResponse"
                }
              }
            },
            "description": "Successful Response"
          }
        },
        "summary": "Begin a passkey step-up for a login MFA challenge",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/mfa/passkey/finish": {
      "post": {
        "description": "Complete a login by redeeming the MFA challenge with a passkey.\n\nTwo independent single-use tokens are spent: ``mfa_token`` (the challenge from\n``/login/email``) and the WebAuthn assertion (``challenge_id`` + ``credential``\nbegun at ``/login/mfa/passkey/begin``). The MFA challenge is consumed first --\nmirroring ``/login/mfa`` -- so one password login grants exactly one step-up\nattempt. The asserted passkey MUST belong to the challenge's developer;\notherwise the login is refused. On success this issues exactly the same\nsession as ``/login/mfa``. Every failure collapses to a single 401 so the\nendpoint reveals nothing about which challenges, accounts, or passkeys exist.",
        "operationId": "mfa_passkey_finish_api_v1_login_mfa_passkey_finish_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MfaPasskeyVerifyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Complete a login MFA challenge with a passkey step-up",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/passkey/begin": {
      "post": {
        "description": "Start a passwordless login.\n\nReturns the ``PublicKeyCredentialRequestOptions`` for\n``navigator.credentials.get()`` and an opaque ``challenge_id`` the client\nechoes back on finish. No account is identified here: ``allowCredentials``\nis empty so the browser offers any discoverable passkey registered for this\nsite, and the developer never types an email.",
        "operationId": "passkey_login_begin_api_v1_login_passkey_begin_post",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticationBeginResponse"
                }
              }
            },
            "description": "Successful Response"
          }
        },
        "summary": "Begin a passwordless passkey login",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/passkey/finish": {
      "post": {
        "description": "Complete a passwordless login by verifying the passkey assertion.\n\nOn success this issues exactly the same session (access JWT + rotating\nrefresh token, mirrored as cookies) as ``/login/email`` -- a passkey login\nand a password login are indistinguishable from that point on. Every\nfailure collapses to a single 401 so the endpoint reveals nothing about\nwhich passkeys or accounts exist.",
        "operationId": "passkey_login_finish_api_v1_login_passkey_finish_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthenticationFinishRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Complete a passwordless passkey login",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/refresh": {
      "post": {
        "description": "Rotate a refresh token: consume the old one, issue a new access + refresh.\n\nThe refresh token may be supplied in the JSON body (refresh_token) or\nvia the dfdeveloper_refresh cookie. Body wins if both are present.\nOn success the old refresh row is marked consumed (single-use rotation)\nand new tokens \u2014 inheriting the same family \u2014 are returned + set as cookies.\n\nIf the supplied token was *already* consumed or revoked, that's a replay of\na rotated token: per the OAuth 2.0 security BCP it's treated as\ntheft, the whole token family is revoked,\nan audit event + metric are emitted, and the response is the same uniform\n401 an unknown token gets \u2014 so an attacker can't distinguish \"reuse detected\"\nfrom \"garbage token\".",
        "operationId": "refresh_api_v1_login_refresh_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Exchange a refresh token for a new access JWT",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/login/sso/exchange": {
      "post": {
        "description": "Redeem an SSO session-handoff code for an access JWT and a refresh token.\n\nThe SAML assertion consumer service mints a single-use code and redirects the\nbrowser to the web app callback; the web app posts that code here\nserver-to-server to obtain the session it stores in its own cookies. It is\norganization-agnostic: the code alone carries the already-authenticated\ndeveloper identity, so there is no organization in the path.\n\nSingle-use: the code is consumed atomically by the first exchange. Every\nfailure mode - unknown, expired, already redeemed, an account pending deletion,\nor a transient storage failure - collapses to the same uniform 401, so the\nendpoint cannot be used as an oracle.",
        "operationId": "sso_exchange_api_v1_login_sso_exchange_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SSOSessionExchangeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Exchange a single-use SSO handoff code for a session",
        "tags": [
          "Login"
        ]
      }
    },
    "/api/v1/mfa": {
      "get": {
        "description": "Return whether 2FA is enabled and how many recovery codes remain.",
        "operationId": "get_status_api_v1_mfa_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MfaStatusResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Status",
        "tags": [
          "MFA"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/mfa/disable": {
      "post": {
        "description": "Disable 2FA (removes the TOTP secret and recovery codes). Requires the password.",
        "operationId": "disable_mfa_api_v1_mfa_disable_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MfaPasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Disable Mfa",
        "tags": [
          "MFA"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/mfa/recovery-codes/regenerate": {
      "post": {
        "description": "Rotate recovery codes (invalidating the old set). Requires the password.",
        "operationId": "regenerate_recovery_codes_api_v1_mfa_recovery_codes_regenerate_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MfaPasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecoveryCodesResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Regenerate Recovery Codes",
        "tags": [
          "MFA"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/mfa/totp/enroll/begin": {
      "post": {
        "description": "Begin TOTP enrollment.\n\nReturns the base32 secret and an ``otpauth://`` provisioning URI (rendered as\na QR code by the client). The secret is shown exactly once; confirm with a\ncode from the authenticator app to activate the factor.",
        "operationId": "begin_totp_enrollment_api_v1_mfa_totp_enroll_begin_post",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TotpEnrollmentBeginResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Begin Totp Enrollment",
        "tags": [
          "MFA"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/mfa/totp/enroll/confirm": {
      "post": {
        "description": "Confirm TOTP enrollment with the first code; return one-time recovery codes.\n\nThe recovery codes are shown only in this response -- the server keeps hashes\n-- so the client must surface them for the user to store.",
        "operationId": "confirm_totp_enrollment_api_v1_mfa_totp_enroll_confirm_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TotpEnrollmentConfirmRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecoveryCodesResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Confirm Totp Enrollment",
        "tags": [
          "MFA"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations": {
      "get": {
        "description": "Return every organization the calling developer has a role on.\n\nThe result is scoped to the caller rather than to one organization, so no\norganization role is required. A Personal Access Token still sees only the\nsingle organization it is bound to.\n\nThe list is not paginated: a developer can belong to at most five\norganizations.",
        "operationId": "get_organizations_api_v1_organizations_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/OrganizationResponse"
                  },
                  "title": "Response Get Organizations Api V1 Organizations Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the info for the organizations the developer has access to",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Create a new organization.",
        "operationId": "create_organization_api_v1_organizations_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewOrganization"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "text/plain": {
                "schema": {
                  "format": "uuid",
                  "type": "string"
                }
              }
            },
            "description": "Organization created. The body is the new organization's UUID as raw text (for example `550e8400-e29b-41d4-a716-446655440000`) \u2014 it is not JSON, and the response carries no `Content-Type` header. Parse it as a plain string."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create a new organization",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}": {
      "delete": {
        "description": "Delete the given organization.",
        "operationId": "delete_organization_api_v1_organizations__organization_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete the given organization",
        "tags": [
          "Organizations"
        ]
      },
      "get": {
        "description": "Get the info for the given organization.",
        "operationId": "get_organization_api_v1_organizations__organization_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the info for an organization",
        "tags": [
          "Organizations"
        ]
      },
      "patch": {
        "description": "Update an organization.",
        "operationId": "update_organization_api_v1_organizations__organization_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchOrganization"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Organization",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/byosa": {
      "get": {
        "description": "Return the organization's current storage backend configuration.\n\nAnswers ``{\"storage_backend\": \"hosted\"}`` for an organization on AppGantry\nhosted storage, and the full bring-your-own-storage-account tuple for an\norganization on its own. It reports the saved configuration only \u2014 nothing\nis called on the storage provider.\n\n**Authentication:** developer JWT plus the organization ``ADMIN`` role.\nPersonal Access Tokens and project access tokens are rejected with 403: the\nstorage-account surface is assisted by AppGantry and accepts only an\ninteractive sign-in.",
        "operationId": "get_byosa_config_api_v1_organizations__organization_id__byosa_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "account_url": {
                      "format": "uri",
                      "type": "string"
                    },
                    "container_name": {
                      "type": "string"
                    },
                    "organization_id": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "storage_backend": {
                      "enum": [
                        "hosted",
                        "byosa"
                      ],
                      "type": "string"
                    },
                    "tenant_id": {
                      "format": "uuid",
                      "type": "string"
                    }
                  },
                  "required": [
                    "organization_id",
                    "storage_backend"
                  ],
                  "title": "StorageBackendConfig",
                  "type": "object"
                }
              }
            },
            "description": "The organization's storage backend. `storage_backend` is `hosted` or `byosa`; the `tenant_id`, `account_url` and `container_name` fields are present only for a `byosa` organization."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Show the org's storage backend configuration",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/byosa/diagnose": {
      "post": {
        "description": "Re-run the storage validation checks against the organization's saved\nconfiguration.\n\nAnswers \"is my storage setup still working?\" on demand and returns the same\nstructured validation report that setup returns on failure. The checks are\nlocked to the target the organization already has configured, so the route\ncannot be used to probe arbitrary tenants.\n\nIt writes nothing, but it does make live calls into your cloud tenant. 404\nwhen the organization is on AppGantry hosted storage \u2014 there is no target\nto diagnose.\n\n**Authentication:** developer JWT plus the organization ``ADMIN`` role.\nPersonal Access Tokens and project access tokens are rejected with 403, even\nthough this is a read, because it reaches into the customer's own cloud\ntenant.",
        "operationId": "diagnose_byosa_api_v1_organizations__organization_id__byosa_diagnose_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BYOSAValidationReport"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Run the validation pipeline against the org's configured BYOSA target",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/byosa/onboarding-request": {
      "get": {
        "description": "Return the organization's most-recent onboarding request and its status.\n\nOn the `validation_failed` branch the response carries a structured\n`validation_report` so the customer can fix and resubmit. 404 when the\norganization has never submitted a request.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "get_onboarding_request_api_v1_organizations__organization_id__byosa_onboarding_request_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "properties": {
                        "error": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error"
                      ],
                      "title": "NoOnboardingRequest",
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Either the organization has never submitted an onboarding request \u2014 a bare `{\"error\": ...}` body \u2014 or the organization does not exist / the caller has no access to it, which answers the standard error envelope. The two are deliberately indistinguishable so the route cannot be used to enumerate organizations."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Show the org's latest BYOSA onboarding request",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "post": {
        "description": "Submit a storage-account onboarding request for the caller's organization.\n\nThe request is queued as ``pending`` and the organization stays on AppGantry\nhosted storage until the switch is approved; activation is assisted rather\nthan instant. Poll the ``GET`` sibling for the validation outcome and the\neventual approval.\n\n**Authentication:** developer JWT plus the organization ``ADMIN`` role;\nPersonal Access Tokens and project access tokens are rejected with 403.",
        "operationId": "submit_onboarding_request_api_v1_organizations__organization_id__byosa_onboarding_request_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OnboardingRequestSubmit"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Submit a self-service BYOSA onboarding request",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/byosa/onboarding-request/{request_id}": {
      "delete": {
        "description": "Withdraw an open onboarding request for the caller's organization.\n\nOrganization-scoped: the caller can only withdraw a request belonging to an\norganization they administer. 409 when the request is already resolved or\nunknown \u2014 the two are deliberately indistinguishable so the route is not an\nenumeration oracle.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "withdraw_onboarding_request_api_v1_organizations__organization_id__byosa_onboarding_request__request_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "request_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Request Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Withdraw a not-yet-resolved BYOSA onboarding request",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/credit/ledger": {
      "get": {
        "description": "Paginated list of the org's prepaid credit movements, newest first.\n\nThe credit panel of the transaction history: signup credit, operator credits,\nPaddle top-ups, and refunds. ``amount_cents`` is the positive magnitude;\n``kind`` (``topup``/``refund``) gives the sign; a refund's ``fee_cents`` is the\nwithheld Paddle fee. Cursor-paginated like the usage-event list: a full page\nreturns an opaque ``next_cursor`` to pass back; null means the last page. A\nmalformed cursor falls back to the first page.\n\nADMIN-gated identically to the usage summary: the credit ledger is the org's\nbilling posture, which an ordinary member shouldn't see.",
        "operationId": "list_credit_ledger_api_v1_organizations__organization_id__credit_ledger_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditLedgerPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List the organization's prepaid credit movements (top-ups, refunds, credits)",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/credit/topup": {
      "post": {
        "description": "Mint a Paddle checkout the customer pays to top up prepaid credit.\n\nThe graduation path off a 402 ``prepaid_credit_exhausted``: a Team org that\nexhausted its grant adds a card and tops up to continue. Returns the Paddle\ntransaction id (+ optional hosted-checkout URL) the frontend opens with\nPaddle.js. The balance does NOT change here \u2014 it moves only when Paddle's\nverified ``transaction.completed`` webhook lands. ADMIN-gated + ALL scope:\ninitiating a payment is a privileged write, not a read.",
        "operationId": "create_credit_topup_api_v1_organizations__organization_id__credit_topup_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopUpRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopUpResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Start a Paddle checkout to add prepaid credit to the organization",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/feature-flags": {
      "get": {
        "description": "Return the resolved value of every active feature flag for this organization.\n\nThe body is a flat ``{flag_key: bool}`` map: each value already has any\norganization-specific setting applied. Archived flags are omitted, and a\nclient should treat a key that is absent as off. This is a read and changes\nnothing.",
        "operationId": "get_organization_feature_flags_api_v1_organizations__organization_id__feature_flags_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationFlagsResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the effective feature-flag set for an organisation",
        "tags": [
          "Feature Flags"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/icon": {
      "delete": {
        "description": "Remove the org's icon. ADMIN-gated; idempotent 204 when none set.",
        "operationId": "delete_organization_icon_api_v1_organizations__organization_id__icon_delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete the organization's icon",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Store the org's icon.\n\nADMIN-gated: an org's branding is an admin concern, like editing its name.\nThe upload body is capped at a fixed size limit (enforced by\nreading one byte past the cap, independent of any client Content-Length), and\nthe true image type is sniffed from the bytes server-side (the declared\nContent-Type is ignored). 204 on success; 400 for a non-image payload; 413\nwhen over the size cap.",
        "operationId": "set_organization_icon_api_v1_organizations__organization_id__icon_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_set_organization_icon_api_v1_organizations__organization_id__icon_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Upload (or replace) the organization's icon",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/invites": {
      "get": {
        "description": "Return non-expired pending invites, newest first, cursor-paginated.\nADMIN-only (the email address is sensitive enough to gate behind admin).",
        "operationId": "list_organization_invites_api_v1_organizations__organization_id__invites_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationInvitePage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List pending organization invites",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Generate an invite + send the email. ADMIN-only.\n\nReturns the invite with its raw ``invite_token`` field populated.\nThe inviter is expected to construct the accept-URL out of it\n(and the email-sending side does the same internally). The token\nappears ONCE in this response \u2014 subsequent GETs omit it.",
        "operationId": "create_organization_invite_api_v1_organizations__organization_id__invites_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewOrganizationInvite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationInvite"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Invite a developer to the organization",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/invites/{invite_id}": {
      "delete": {
        "description": "Revoke a pending invite. ADMIN-only.",
        "operationId": "revoke_organization_invite_api_v1_organizations__organization_id__invites__invite_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke a pending organization invite",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/invites/{invite_id}/resend": {
      "post": {
        "description": "Re-send a pending invite's email and rotate its token + 14-day window,\nkeeping the invite id stable so the WebApp's pending list stays consistent. ADMIN-only. Throttled per invite; the response omits the raw token\n(it went out by email).",
        "operationId": "resend_organization_invite_api_v1_organizations__organization_id__invites__invite_id__resend_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationInvite"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Re-send a pending organization invite",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/leave": {
      "post": {
        "description": "Remove yourself from an organization.\n\nAny member may leave (gated on org READ - the same gate the member listing\nuses). Last-admin-guarded: a\nsole admin must transfer ownership (promote another member to ADMIN) before\nleaving, otherwise the org would be orphaned. Full PAT scope is\nrequired so a narrowly-scoped machine token can't offboard the human.",
        "operationId": "leave_organization_api_v1_organizations__organization_id__leave_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Leave an organization (self-service offboarding)",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/members": {
      "get": {
        "description": "Return the org's members with their roles, newest first, cursor-paginated. Was a bare array silently truncated at 1000; now the full set is\nreachable page by page via ``next_cursor``.\n\nOrg READ is enough \u2014 every member can see the membership list.\nMutations require ADMIN below.",
        "operationId": "list_organization_members_api_v1_organizations__organization_id__members_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List organization members",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/members/{developer_id}": {
      "delete": {
        "description": "Remove a member. ADMIN-only. Last-admin-guarded.",
        "operationId": "remove_organization_member_api_v1_organizations__organization_id__members__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove a member from an organization",
        "tags": [
          "Organizations"
        ]
      },
      "patch": {
        "description": "Change a member's role. ADMIN-only.\n\nLast-admin guard: refuses to demote the final ADMIN below ADMIN.",
        "operationId": "change_organization_member_role_api_v1_organizations__organization_id__members__developer_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeOrganizationMemberRole"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Change an organization member's role",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/mfa-policy": {
      "get": {
        "description": "Return whether this org mandates MFA and when enforcement begins.\n\nADMIN-gated like the other org-governance surfaces: the require-MFA policy is\na security posture only an org admin manages. ``enforced_at`` is null when the\npolicy is off; while it is set but still in the future the org is inside its\ngrace window.",
        "operationId": "get_organization_mfa_policy_api_v1_organizations__organization_id__mfa_policy_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrgMfaPolicyResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a token that lacks the scope this route requires."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the organization's require-MFA policy",
        "tags": [
          "Organizations"
        ]
      },
      "put": {
        "description": "Enable or disable the org-level require-MFA policy.\n\nADMIN-gated + ALL scope: mandating MFA changes how every member authenticates,\nso it is a privileged write. Enabling forces all members to have TOTP 2FA\nenrolled to keep using the API; ``grace_period_days`` (0 = immediate) gives\nexisting members a window to enrol before they are gated. Disabling clears the\npolicy. Attributed to the acting admin in the audit trail.",
        "operationId": "set_organization_mfa_policy_api_v1_organizations__organization_id__mfa_policy_put",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrgMfaPolicyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrgMfaPolicyResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the request authenticated with a token that lacks the scope this route requires."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Enable or disable the organization's require-MFA policy",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/payment-method": {
      "post": {
        "description": "Mint a checkout the customer completes to save a card.\n\nNothing is charged here. Returns the payment provider's transaction id and,\nwhen available, a hosted-checkout URL for the client to open.\n``card_on_file`` flips true only once the provider confirms the completed\ntransaction, which happens after the customer finishes the checkout, not when\nthis call returns. Requires the organization ``ADMIN`` role and full token\nscope: adding a payment method is a privileged write.",
        "operationId": "create_payment_method_api_v1_organizations__organization_id__payment_method_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardCaptureResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Start a Paddle checkout to put a card on file for the organization",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/payment-method/auth-token": {
      "post": {
        "description": "Mint a short-lived Paddle customer auth token for saved-card checkout.\n\nPaddle only presents a customer's saved payment methods in a checkout when\nPaddle.js is given a ``customerAuthToken``; attaching the transaction's\n``customer_id`` alone does not surface them. The frontend calls this right\nbefore opening a one-page top-up/card checkout so a returning customer can pick\nthe card they already saved instead of re-keying it. Returns 200 with a null\n``customer_auth_token`` when the org has no Paddle customer on file (never\nonboarded / no saved card) - \"no saved card\" is a normal state, so the frontend\nopens a plain checkout rather than treating a 404 as an error. ADMIN-gated\n+ ALL scope: it authorises acting on the org's stored payment methods, a\nprivileged capability, not a read.",
        "operationId": "create_payment_method_auth_token_api_v1_organizations__organization_id__payment_method_auth_token_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerAuthTokenResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Mint a Paddle customer auth token so a checkout can offer the card on file",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects": {
      "get": {
        "description": "Get the projects in the organization.",
        "operationId": "get_projects_api_v1_organizations__organization_id__projects_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Project"
                  },
                  "title": "Response Get Projects Api V1 Organizations  Organization Id  Projects Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Projects",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Add a new project to the organisation's workspace.",
        "operationId": "create_project_api_v1_organizations__organization_id__projects_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewProject"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "title": "Response Create Project Api V1 Organizations  Organization Id  Projects Post",
                  "type": "string"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}": {
      "delete": {
        "description": "Delete the given project.",
        "operationId": "delete_project_api_v1_organizations__organization_id__projects__project_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Project",
        "tags": [
          "Projects"
        ]
      },
      "get": {
        "description": "Get the info for the given project.",
        "operationId": "get_project_api_v1_organizations__organization_id__projects__project_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Project",
        "tags": [
          "Projects"
        ]
      },
      "patch": {
        "description": "Update a project.",
        "operationId": "update_project_api_v1_organizations__organization_id__projects__project_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchProject"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/icon": {
      "delete": {
        "description": "Remove the project's icon. ADMIN-gated; idempotent 204 when none set.",
        "operationId": "delete_project_icon_api_v1_organizations__organization_id__projects__project_id__icon_delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete the project's icon",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Store the project's icon.\n\nADMIN-gated: a project's branding is an admin concern, like editing its name.\nThe upload body is capped at a fixed size limit (enforced by\nreading one byte past the cap, independent of any client Content-Length), and\nthe true image type is sniffed from the bytes server-side (the declared\nContent-Type is ignored). 204 on success; 400 for a non-image payload; 413\nwhen over the size cap.",
        "operationId": "set_project_icon_api_v1_organizations__organization_id__projects__project_id__icon_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_set_project_icon_api_v1_organizations__organization_id__projects__project_id__icon_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Upload (or replace) the project's icon",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/invites": {
      "get": {
        "description": "Return non-expired pending invites, newest first, cursor-paginated.\nProject ADMIN.",
        "operationId": "list_project_invites_api_v1_organizations__organization_id__projects__project_id__invites_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInvitePage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List pending project invites",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Generate a project invite + (later) send the email. Project ADMIN.",
        "operationId": "create_project_invite_api_v1_organizations__organization_id__projects__project_id__invites_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewProjectInvite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInvite"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Invite a developer to the project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/invites/{invite_id}": {
      "delete": {
        "description": "Revoke a pending project invite. Project ADMIN.",
        "operationId": "revoke_project_invite_api_v1_organizations__organization_id__projects__project_id__invites__invite_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke a pending project invite",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/invites/{invite_id}/resend": {
      "post": {
        "description": "Re-send a pending project invite's email and rotate its token + 14-day\nwindow, keeping the invite id stable. Project ADMIN. Throttled per\ninvite; the response omits the raw token (it went out by email).",
        "operationId": "resend_project_invite_api_v1_organizations__organization_id__projects__project_id__invites__invite_id__resend_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInvite"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Re-send a pending project invite",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/members": {
      "get": {
        "description": "Return the project's members with their roles, newest first, cursor-paginated. Was a bare array silently truncated at 1000; now the full set is\nreachable page by page via ``next_cursor``. Project READ.",
        "operationId": "list_project_members_api_v1_organizations__organization_id__projects__project_id__members_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List project members",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/members/{developer_id}": {
      "delete": {
        "description": "Remove a project member. Project ADMIN; last-admin guarded.",
        "operationId": "remove_project_member_api_v1_organizations__organization_id__projects__project_id__members__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove a member from a project",
        "tags": [
          "Projects"
        ]
      },
      "patch": {
        "description": "Change a member's project role. Project ADMIN; last-admin guarded.",
        "operationId": "change_project_member_role_api_v1_organizations__organization_id__projects__project_id__members__developer_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeProjectMemberRole"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Change a project member's role",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/testers": {
      "get": {
        "description": "List project-tier testers \u2014 active + pending merged, newest first,\ncursor-paginated. Project ADMIN-only. Was a bare array truncated at\n1000; active and pending now interleave by timestamp.",
        "operationId": "list_project_testers_api_v1_organizations__organization_id__projects__project_id__testers_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List project testers (active + pending)",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Direct-add a tester at project tier by email. Project ADMIN-only.",
        "operationId": "add_project_tester_api_v1_organizations__organization_id__projects__project_id__testers_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddTesterRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Add a tester to the project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/testers/pending/{pending_id}": {
      "delete": {
        "description": "Cancel a pending project-tier tester invite.",
        "operationId": "remove_pending_project_tester_api_v1_organizations__organization_id__projects__project_id__testers_pending__pending_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Cancel a pending project tester grant",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/testers/pending/{pending_id}/resend": {
      "post": {
        "description": "Re-send a pending project-tier tester grant's notification email, keeping\nthe pending-grant id stable so the WebApp's pending list stays consistent. ADMIN-only. Throttled per grant; a missing/already-accepted grant\nreturns a uniform 404 (anti-enumeration). No token is issued, so the response\nbody is empty.",
        "operationId": "resend_pending_project_tester_api_v1_organizations__organization_id__projects__project_id__testers_pending__pending_id__resend_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Re-send a pending project tester grant",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/projects/{project_id}/testers/{developer_id}": {
      "delete": {
        "description": "Revoke an active project-tier tester grant.",
        "operationId": "remove_project_tester_api_v1_organizations__organization_id__projects__project_id__testers__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove an active project tester",
        "tags": [
          "Projects"
        ]
      },
      "get": {
        "description": "Detail for one active project-tier tester, for the WebApp tester detail\npage. Project ADMIN-only, PAT READ scope (mirrors the list). Returns the\ntester's direct project grant plus their org-level tester groups. A\ndeveloper who is not a direct active project tester returns a uniform 404\n(anti-enumeration, matching the list/DELETE surfaces).",
        "operationId": "get_project_tester_api_v1_organizations__organization_id__projects__project_id__testers__developer_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterDetailResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get a single project tester",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/spend-cap": {
      "get": {
        "description": "Return the org's operator ceiling, customer self-limit, and effective cap.\n\nADMIN-gated like the other org-governance surfaces. The operator ceiling is\nread-only to the customer (set by an operator on the admin pod); the customer\nself-limit is settable here via PUT. ``effective_spend_cap_cents`` is what the\npre-mint gate enforces: ``min`` of the two ignoring NULLs, null only when both\nare unset.",
        "operationId": "get_organization_spend_cap_api_v1_organizations__organization_id__spend_cap_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCapView"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the organization's monthly spend caps",
        "tags": [
          "Organizations"
        ]
      },
      "put": {
        "description": "Set / raise / lower / clear the org's own monthly spend self-limit (customer cap).\n\nADMIN-gated + ALL scope: it changes the org's billing exposure, so it is a\nprivileged write. This is the CUSTOMER self-limit only \u2014 it can never raise\nexposure above the operator ceiling (the pre-mint gate enforces\n``min(operator, customer)``), and it cannot touch the operator ceiling itself.\n``customer_spend_cap_cents = null`` clears the self-limit. Attributed to the\nacting admin (``editor_id = developer_id``) in the audit trail. Returns the\nrefreshed cap view.",
        "operationId": "set_organization_spend_cap_api_v1_organizations__organization_id__spend_cap_put",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerSpendCapRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpendCapView"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Set or clear the organization's customer monthly self-limit",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/sso": {
      "delete": {
        "description": "Remove the org's SAML SSO connection. Idempotent \u2014 204 even if none exists.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "delete_sso_connection_api_v1_organizations__organization_id__sso_delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete the org's SAML SSO connection",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "get": {
        "description": "Return the org's SAML SSO connection, or 404 when none is configured.\n\n**Authentication:** developer JWT (bearer token or session cookie) plus the\norganization `ADMIN` role. Personal Access Tokens and project access tokens\nare rejected with 403 \u2014 SSO config governs how members authenticate, so it is\nas privileged as the storage-account surface and follows the same JWT-only\nrule.",
        "operationId": "get_sso_connection_api_v1_organizations__organization_id__sso_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoConnectionResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "properties": {
                        "error": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error"
                      ],
                      "title": "NotConfigured",
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Either no SSO connection is configured for this organization \u2014 a bare `{\"error\": ...}` body \u2014 or the organization does not exist / the caller has no access to it, which answers the standard error envelope. The two are deliberately indistinguishable so the route cannot be used to enumerate organizations."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Show the org's SAML SSO connection",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "put": {
        "description": "Create or replace the org's SAML SSO connection (Business only).\n\nRefused with `operation_not_allowed` for a Team org \u2014 that tier does not\ninclude SSO. Attributed to the acting admin in the audit trail.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "configure_sso_connection_api_v1_organizations__organization_id__sso_put",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SsoConnectionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoConnectionResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Configure (create or replace) the org's SAML SSO connection",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/acs": {
      "post": {
        "description": "Consume a signed SAML response and hand the session to the web app.\n\n**Authentication: none \u2014 the SAML signature IS the credential.** The identity\nprovider POSTs the assertion here as `application/x-www-form-urlencoded`; this\nis not a route a customer integration calls.\n\nThe service-provider ACS lives on the API host, but the browser session is\nowned by the web app on a different host, so cookies set here would never\nreach it. On success we mint a short-lived single-use handoff code and 303 the\nbrowser to the web app callback carrying the raw code; the web app exchanges\nit server-to-server at ``POST /api/v1/login/sso/exchange`` for session tokens.\nOn ANY failure the browser is redirected to a single uniform error URL.",
        "operationId": "sso_acs_api_v1_organizations__organization_id__sso_acs_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "303": {
            "description": "Redirect the browser onward. On success the target is the web app's SSO callback carrying a single-use handoff code; on ANY failure it is one uniform error URL, so the outcome is never an enumeration oracle.",
            "headers": {
              "Location": {
                "description": "The web app callback URL, or the uniform SSO error URL.",
                "schema": {
                  "format": "uri",
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "SAML Assertion Consumer Service (ACS)",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/sso/domains": {
      "get": {
        "description": "Return the org's email-domain claims, verified and pending.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "list_domain_verifications_api_v1_organizations__organization_id__sso_domains_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoDomainListResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List the org's email-domain claims",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "post": {
        "description": "Claim an email domain and return the DNS TXT record to publish.\n\nBusiness only (refused with `operation_not_allowed` for a Team\norg). Attributed to the acting admin in the audit trail.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "initiate_domain_verification_api_v1_organizations__organization_id__sso_domains_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SsoDomainVerificationRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoDomainResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Initiate DNS-TXT verification of an email domain",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/domains/{domain}": {
      "delete": {
        "description": "Remove an organization's domain claim. Idempotent \u2014 204 even if none exists.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "remove_domain_verification_api_v1_organizations__organization_id__sso_domains__domain__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "domain",
            "required": true,
            "schema": {
              "title": "Domain",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove an email-domain claim",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/domains/{domain}/verify": {
      "post": {
        "description": "Resolve the domain's TXT records and flag the claim verified if present.\n\nBusiness only. Returns a uniform validation error if the record\nis absent or the domain is already verified by another organization (no\nownership oracle). 404 if no verification was initiated for the domain.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "verify_domain_api_v1_organizations__organization_id__sso_domains__domain__verify_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "domain",
            "required": true,
            "schema": {
              "title": "Domain",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoDomainResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Check the DNS TXT record and mark the domain verified",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/enabled": {
      "put": {
        "description": "Toggle the org's SSO enabled flag. Enabling is Business only.\n\nDisabling is allowed regardless of tier so a downgraded org can still turn\nSSO off. 404 if no connection is configured.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "set_sso_enabled_api_v1_organizations__organization_id__sso_enabled_put",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SsoEnabledRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoConnectionResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Enable or disable the org's SAML SSO connection",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/login": {
      "get": {
        "description": "Redirect the browser to the org's identity provider with a SAML AuthnRequest.\n\n**Authentication: none.** This is the entry point to logging in, so it is\nreached by an unauthenticated browser. Navigate to it \u2014 do not fetch it as\nJSON. 404 if the organization has no enabled SSO connection.\n\nThe request id is recorded server-side so the assertion consumer service can\nredeem it single-use for replay protection. A random state token travels as\nthe SAML RelayState and is also set as an HttpOnly cookie on the API host; the\nassertion consumer service requires the echoed RelayState to equal the cookie\nbefore minting a session, binding the login to the browser that began it.",
        "operationId": "sso_login_api_v1_organizations__organization_id__sso_login_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the organization's identity provider carrying the SAML AuthnRequest. Also sets the single-use SSO state cookie, scoped to this organization's SSO paths.",
            "headers": {
              "Location": {
                "description": "The identity provider's single sign-on URL.",
                "schema": {
                  "format": "uri",
                  "type": "string"
                }
              },
              "Set-Cookie": {
                "description": "The HttpOnly SSO state cookie the assertion consumer service checks.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The organization has no enabled SSO connection."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The SAML authentication request could not be built."
          }
        },
        "summary": "Begin SP-initiated SAML SSO login",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/sso/logout": {
      "get": {
        "description": "Return the identity provider redirect URL that begins SP-initiated Single Logout.\n\nThe web app calls this server-to-server with the developer's access JWT, then\nredirects the browser to the returned URL so the identity provider tears down\nits session. The developer's email is the LogoutRequest NameID (the\nemailAddress NameID format). 404 if the organization has no enabled SSO\nconnection; `operation_not_allowed` if Single Logout is not configured. The\nlocal session is ended by the web app's own logout, not here.\n\n**Authentication:** developer JWT plus the organization `READ` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "sso_logout_api_v1_organizations__organization_id__sso_logout_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoLogoutResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Begin SP-initiated SAML Single Logout",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/metadata": {
      "get": {
        "description": "Return the SP metadata XML the customer registers in their identity provider.\n\nThe response body is XML (`application/samlmetadata+xml`), not JSON.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "get_sp_metadata_api_v1_organizations__organization_id__sso_metadata_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/samlmetadata+xml": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "The service-provider SAML metadata descriptor, served as `application/samlmetadata+xml`. Upload or paste it into the identity provider."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Return the SP SAML metadata descriptor for the org",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/require-sso": {
      "put": {
        "description": "Toggle whether members must authenticate via SSO.\n\nEnabling requires an SSO-eligible tier, an enabled connection, and at least\none verified email domain; disabling is always allowed. 404 if no connection\nis configured.\n\n**Authentication:** developer JWT plus the organization `ADMIN` role; PATs and\nproject access tokens are rejected with 403.",
        "operationId": "set_require_sso_api_v1_organizations__organization_id__sso_require_sso_put",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SsoRequireRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoConnectionResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Enable or disable the org's require-SSO enforcement policy",
        "tags": [
          "Organizations"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/organizations/{organization_id}/sso/slo": {
      "get": {
        "description": "Handle an inbound SAML Single Logout message over the HTTP-Redirect binding.\n\n**Authentication: none \u2014 the SAML signature IS the credential** (inbound SLO\nmessages must be signed). The message and its signature arrive in the query\nstring; this is not a route a customer integration calls.\n\nTwo directions:\n\n* An identity-provider-initiated ``SAMLRequest`` (LogoutRequest): we validate\n  it, revoke the named developer's sessions, and redirect the browser to the\n  identity provider's SLO endpoint carrying our signed LogoutResponse.\n* A ``SAMLResponse`` (the provider's ack to our own LogoutRequest): we\n  validate it and redirect the browser to the web app's logged-out page.\n\nOn ANY failure the browser is redirected to a single uniform error URL so the\noutcome is not a signature / enumeration oracle, mirroring the ACS.",
        "operationId": "sso_slo_api_v1_organizations__organization_id__sso_slo_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "303": {
            "description": "Redirect the browser onward: back to the identity provider carrying our signed LogoutResponse, to the web app's logged-out page, or \u2014 on ANY failure \u2014 to one uniform error URL.",
            "headers": {
              "Location": {
                "description": "The identity provider SLO endpoint, the logged-out page, or the error URL.",
                "schema": {
                  "format": "uri",
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "SAML Single Logout Service (SLO)",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups": {
      "get": {
        "description": "List the org's tester groups, newest first, cursor-paginated. ADMIN-only.",
        "operationId": "list_tester_groups_api_v1_organizations__organization_id__tester_groups_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterGroupPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List tester groups",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Create a named, reusable tester group. ADMIN-only.",
        "operationId": "create_tester_group_api_v1_organizations__organization_id__tester_groups_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTesterGroupRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterGroupResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create a tester group",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups/{group_id}": {
      "delete": {
        "description": "Delete a group; members + grants cascade. ADMIN-only.",
        "operationId": "delete_tester_group_api_v1_organizations__organization_id__tester_groups__group_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete a tester group",
        "tags": [
          "Organizations"
        ]
      },
      "get": {
        "operationId": "get_tester_group_api_v1_organizations__organization_id__tester_groups__group_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterGroupResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get a tester group",
        "tags": [
          "Organizations"
        ]
      },
      "patch": {
        "description": "Rename a tester group and/or flip its release-notification toggle.\n\nADMIN-only; name unique per org. At least one field must be supplied (the\nrequest schema rejects an empty body).",
        "operationId": "update_tester_group_api_v1_organizations__organization_id__tester_groups__group_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTesterGroupRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterGroupResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update a tester group",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups/{group_id}/grants": {
      "get": {
        "operationId": "list_tester_group_grants_api_v1_organizations__organization_id__tester_groups__group_id__grants_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/TesterGroupGrantResponse"
                  },
                  "title": "Response List Tester Group Grants Api V1 Organizations  Organization Id  Tester Groups  Group Id  Grants Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List a tester group's scope grants",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Point the group at an org/project/channel scope; members inherit access.",
        "operationId": "grant_tester_group_api_v1_organizations__organization_id__tester_groups__group_id__grants_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantGroupRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterGroupGrantResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Grant a tester group access at a scope",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups/{group_id}/grants/{grant_id}": {
      "delete": {
        "operationId": "revoke_tester_group_grant_api_v1_organizations__organization_id__tester_groups__group_id__grants__grant_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "grant_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Grant Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke a tester group's scope grant",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups/{group_id}/members": {
      "get": {
        "operationId": "list_group_members_api_v1_organizations__organization_id__tester_groups__group_id__members_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List tester group members (active + pending)",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Add a member by email. Existing developer \u2192 active; unknown \u2192 pending.",
        "operationId": "add_group_member_api_v1_organizations__organization_id__tester_groups__group_id__members_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddTesterRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Add a member to a tester group",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups/{group_id}/members/pending/{pending_id}": {
      "delete": {
        "operationId": "remove_pending_group_member_api_v1_organizations__organization_id__tester_groups__group_id__members_pending__pending_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove a pending tester group member",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tester-groups/{group_id}/members/{developer_id}": {
      "delete": {
        "operationId": "remove_group_member_api_v1_organizations__organization_id__tester_groups__group_id__members__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "group_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Group Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove an active tester group member",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/testers": {
      "get": {
        "description": "List org-tier testers \u2014 accepted memberships and pending grants merged,\nnewest first, cursor-paginated. ADMIN-only. Was a bare array truncated\nat 1000; active and pending now interleave by timestamp.",
        "operationId": "list_organization_testers_api_v1_organizations__organization_id__testers_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List organization testers (active + pending)",
        "tags": [
          "Organizations"
        ]
      },
      "post": {
        "description": "Direct-add a tester at org tier by email. ADMIN-only.\n\nIf a developer with this email exists, the tester membership row\nlands immediately. If not, the email goes into a pending grant\nthat auto-attaches on email verification later. Either way an\nemail notification is sent.",
        "operationId": "add_organization_tester_api_v1_organizations__organization_id__testers_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddTesterRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Add a tester to the organization",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/testers/pending/{pending_id}": {
      "delete": {
        "description": "Cancel a pending org-tier tester invite (before the developer signed up).",
        "operationId": "remove_pending_organization_tester_api_v1_organizations__organization_id__testers_pending__pending_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Cancel a pending organization tester grant",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/testers/pending/{pending_id}/resend": {
      "post": {
        "description": "Re-send a pending org-tier tester grant's notification email, keeping the\npending-grant id stable so the WebApp's pending list stays consistent.\nADMIN-only. Throttled per grant; a missing/already-accepted grant returns a\nuniform 404 (anti-enumeration). No token is issued (the grant auto-attaches on\nsignup by email), so the response body is empty.",
        "operationId": "resend_pending_organization_tester_api_v1_organizations__organization_id__testers_pending__pending_id__resend_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Re-send a pending organization tester grant",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/testers/{developer_id}": {
      "delete": {
        "description": "Revoke an active org-tier tester grant. ADMIN-only.",
        "operationId": "remove_organization_tester_api_v1_organizations__organization_id__testers__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove an active organization tester",
        "tags": [
          "Organizations"
        ]
      },
      "get": {
        "description": "Detail for one active org-tier tester, for the WebApp tester detail page.\nADMIN-only, PAT READ scope (mirrors the list). Returns the tester's direct\ngrant (email, granted_at, granter) plus the org-level tester groups they\nbelong to. A developer who is not a direct active org tester returns a\nuniform 404 (anti-enumeration, matching the list/DELETE surfaces).",
        "operationId": "get_organization_tester_api_v1_organizations__organization_id__testers__developer_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterDetailResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get a single organization tester",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/tier": {
      "post": {
        "description": "Self-serve move of the caller's own org between Team and Business.\n\nADMIN-gated + ALL scope: changing the tier changes how the org is billed, so\nit is a privileged write, not a read. A downgrade, or an upgrade of an org that already has a card on\nfile, completes synchronously (``status='changed'``). An uncarded org asking\nto upgrade to Business gets a card-capture checkout back\n(``status='checkout_required'``) and its tier is left unchanged - Business is\npre-paid like Team, but its higher monthly platform fee is charged in advance\nto a card on file, so one must be captured first. BYOSA and already-target-tier orgs are refused with the\ncanonical message.",
        "operationId": "change_organization_tier_api_v1_organizations__organization_id__tier_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SelfServeTierChangeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SelfServeTierChangeResult"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Change the organization's tier (Team <-> Business)",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/transfer-ownership": {
      "post": {
        "description": "Grant ADMIN (co-ownership) to an existing member. ADMIN-only.\n\nOwnership here is the ADMIN role flag (no single-owner column; multiple\nadmins are allowed), so this promotes the named member to ADMIN. Promote-\nonly: the caller keeps their own ADMIN role and steps down separately (a\nrole change, or by leaving). The main use is letting a sole admin designate\na successor so they can then leave or delete their account.\nIdempotent (204) when the target is already an admin.",
        "operationId": "transfer_organization_ownership_api_v1_organizations__organization_id__transfer_ownership_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransferOrganizationOwnership"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Transfer organization ownership to another member",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/usage": {
      "get": {
        "description": "Return the org's metered usage (upload / download / storage) and prepaid\nbalance for the current billing period \u2014 the customer usage dashboard.\n\nADMIN-gated: this exposes the org's billing posture (prepaid balance + spend),\nwhich is at least as sensitive as the ADMIN-only invite list and the\nmember-hidden ``max_projects`` \u2014 so an ordinary member shouldn't see it. The\nauth dependency rejects non-admins, non-members, and PATs without READ scope\nbefore the body runs.",
        "operationId": "get_usage_summary_api_v1_organizations__organization_id__usage_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageSummaryResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the organization's current-period usage and prepaid balance",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/usage/estimate": {
      "get": {
        "description": "Server-side cost estimate for hypothetical byte volumes.\n\nBacks the frontend's pre-flight estimate (an upload's cost, a single\ndownload's cost, the full-month storage projection) so the\nclient never hardcodes the rate card. Pass any subset of ``upload_bytes`` /\n``download_bytes`` / ``storage_bytes``; each omitted meter returns ``null``\ncost. ``storage_bytes`` is costed as a whole GB-month (\"if it stays put all\nmonth\"), not a month-to-date integral. BYOSA orgs are unmetered, so every\ncost is ``0`` and ``metered`` is ``false``.\n\nADMIN-gated like the usage summary: the rate card and BYOSA status are part\nof the org's billing posture, which an ordinary member shouldn't see. The\n``ge=0`` query constraints reject negative byte counts with a 400 before the\nbody runs.",
        "operationId": "estimate_usage_cost_api_v1_organizations__organization_id__usage_estimate_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "description": "Bytes for an upload-cost estimate.",
            "in": "query",
            "name": "upload_bytes",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "minimum": 0,
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Bytes for an upload-cost estimate.",
              "title": "Upload Bytes"
            }
          },
          {
            "description": "Bytes for a download-cost estimate.",
            "in": "query",
            "name": "download_bytes",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "minimum": 0,
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Bytes for a download-cost estimate.",
              "title": "Download Bytes"
            }
          },
          {
            "description": "Bytes for a full-month storage-cost projection.",
            "in": "query",
            "name": "storage_bytes",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "minimum": 0,
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Bytes for a full-month storage-cost projection.",
              "title": "Storage Bytes"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageEstimateResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Estimate the cost of hypothetical upload / download / storage byte volumes",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/usage/events": {
      "get": {
        "description": "Paginated list of the org's individual usage events, newest first.\n\nThe per-row transaction history behind the usage dashboard: every upload and\ndownload (storage is billed as a period integral, so storage rows carry a\nnull per-event cost). Each row carries its server-derived cost so the client\nnever hardcodes the rate card. Filter by ``meter`` and by an ``occurred_at``\nwindow (half-open ``[after, before)``).\n\nBidirectional keyset pagination: ``next_cursor`` walks toward older\nrows, ``prev_cursor`` toward newer rows. Pass a cursor back as ``cursor=...``\nwith ``direction=next`` (older) or ``direction=prev`` (newer). ``next_cursor``\nis null on the oldest page, ``prev_cursor`` null on the newest (first) page. A\nmalformed cursor falls back to the first (newest) page, and ``direction=prev``\nwith no usable cursor does the same rather than paging from the oldest end.\n\nADMIN-gated identically to the usage summary: this exposes the org's billing\nactivity, which an ordinary member shouldn't see.",
        "operationId": "list_usage_events_api_v1_organizations__organization_id__usage_events_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "description": "Filter to one meter kind.",
            "in": "query",
            "name": "meter",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "enum": [
                    "upload",
                    "download",
                    "storage"
                  ],
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter to one meter kind.",
              "title": "Meter"
            }
          },
          {
            "description": "Inclusive lower bound on occurred_at (ISO 8601).",
            "in": "query",
            "name": "occurred_after",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "format": "date-time",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Inclusive lower bound on occurred_at (ISO 8601).",
              "title": "Occurred After"
            }
          },
          {
            "description": "Exclusive upper bound on occurred_at (ISO 8601).",
            "in": "query",
            "name": "occurred_before",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "format": "date-time",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Exclusive upper bound on occurred_at (ISO 8601).",
              "title": "Occurred Before"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          },
          {
            "description": "Cursor direction: 'next' walks toward older rows, 'prev' toward newer rows.",
            "in": "query",
            "name": "direction",
            "required": false,
            "schema": {
              "default": "next",
              "description": "Cursor direction: 'next' walks toward older rows, 'prev' toward newer rows.",
              "enum": [
                "next",
                "prev"
              ],
              "title": "Direction",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageEventPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List the organization's individual usage events (transaction history)",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/webhooks": {
      "get": {
        "description": "List an org's webhook subscriptions, newest first, cursor-paginated.\nThe signing secret is never included.",
        "operationId": "list_webhooks_api_v1_organizations__organization_id__webhooks_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Webhooks",
        "tags": [
          "Webhooks"
        ]
      },
      "post": {
        "description": "Register a new outbound webhook for the organization.\n\nThe response carries the signing secret EXACTLY ONCE \u2014 the receiver must\nrecord it now to verify the ``X-AppGantry-Signature`` header on deliveries.\nIt is never retrievable again.",
        "operationId": "create_webhook_api_v1_organizations__organization_id__webhooks_post",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewWebhookSubscription"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionCreated"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Webhook",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/webhooks/deliveries": {
      "get": {
        "description": "List an org's recent delivery attempts, newest first, cursor-paginated.\nShows delivery outcome + retry state; the signed payload is never echoed.",
        "operationId": "list_webhook_deliveries_api_v1_organizations__organization_id__webhooks_deliveries_get",
        "parameters": [
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Webhook Deliveries",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/webhooks/{subscription_id}": {
      "delete": {
        "description": "Delete a webhook subscription. Pending deliveries are retained (their\n``subscription_id`` goes NULL) for forensic inspection.",
        "operationId": "delete_webhook_api_v1_organizations__organization_id__webhooks__subscription_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "subscription_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Subscription Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Webhook",
        "tags": [
          "Webhooks"
        ]
      },
      "get": {
        "description": "Get a single webhook subscription (no signing secret).",
        "operationId": "get_webhook_api_v1_organizations__organization_id__webhooks__subscription_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "subscription_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Subscription Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Webhook",
        "tags": [
          "Webhooks"
        ]
      },
      "patch": {
        "description": "Patch a subscription's url / event_types / active / description.\nThe signing secret is not touched here \u2014 rotate it via\n``POST .../{subscription_id}/rotate-secret`` (which keeps the id stable)\nor delete + recreate.",
        "operationId": "update_webhook_api_v1_organizations__organization_id__webhooks__subscription_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "subscription_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Subscription Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchWebhookSubscription"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Webhook",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/webhooks/{subscription_id}/rotate-secret": {
      "post": {
        "description": "Rotate a subscription's signing secret; return the NEW secret EXACTLY ONCE.\n\nGenerates a fresh HMAC signing secret and returns it inline, mirroring the\ncreate response (and the PAT / project-token rotate pattern) \u2014 the receiver\nmust record it now to keep verifying the ``X-AppGantry-Signature`` header,\nas it is never retrievable again. The subscription id and all other config\nstay stable, so receivers keyed on the id keep working across the rotation.\n\nThere is no grace window: every delivery signed after this call uses the new\nsecret and the old one stops being honoured immediately, so the receiver\nmust be ready with the new secret before rotating. ADMIN only.",
        "operationId": "rotate_webhook_secret_api_v1_organizations__organization_id__webhooks__subscription_id__rotate_secret_post",
        "parameters": [
          {
            "in": "path",
            "name": "subscription_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Subscription Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionCreated"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Rotate Webhook Secret",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/v1/organizations/{organization_id}/webhooks/{subscription_id}/test": {
      "post": {
        "description": "Enqueue a synthetic ``webhook.test`` delivery to this subscription.\n\nLets an admin verify the receiver's connectivity + signature handling on\ndemand, without waiting for a real distribution event. The delivery is signed\nand sent through the normal drainer pipeline (so even a paused subscription is\nstill tested) and shows up in ``GET .../webhooks/deliveries``; poll there \u2014 or\nwatch the returned ``delivery_id`` \u2014 for the outcome. Returns 202: the send is\nasynchronous, not completed within this request.",
        "operationId": "send_test_webhook_api_v1_organizations__organization_id__webhooks__subscription_id__test_post",
        "parameters": [
          {
            "in": "path",
            "name": "subscription_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Subscription Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestSendResult"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Send Test Webhook",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/v1/passkeys": {
      "get": {
        "description": "List the calling developer's registered passkeys.",
        "operationId": "list_passkeys_api_v1_passkeys_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/PasskeySummary"
                  },
                  "title": "Response List Passkeys Api V1 Passkeys Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Passkeys",
        "tags": [
          "Passkeys"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/passkeys/register/begin": {
      "post": {
        "description": "Begin registering a new passkey for the calling developer.\n\nReturns the ``PublicKeyCredentialCreationOptions`` for\n``navigator.credentials.create()`` and an opaque ``challenge_id`` the client\nechoes back on finish.",
        "operationId": "begin_registration_api_v1_passkeys_register_begin_post",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistrationBeginResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Bad request. Returned with `error: \"quota_exceeded\"` when the developer already holds the maximum number of passkeys; delete one before registering another."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Begin Registration",
        "tags": [
          "Passkeys"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/passkeys/register/finish": {
      "post": {
        "description": "Complete passkey registration by verifying the attestation response.",
        "operationId": "finish_registration_api_v1_passkeys_register_finish_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegistrationFinishRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasskeySummary"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Finish Registration",
        "tags": [
          "Passkeys"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/passkeys/{passkey_id}": {
      "delete": {
        "description": "Delete one of the calling developer's passkeys.\n\nOwner-scoped like rename. Removing every passkey from an account that has no\npassword set would force account recovery, so clients should warn before the\nlast one is deleted; the API itself does not block it.",
        "operationId": "delete_passkey_api_v1_passkeys__passkey_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "passkey_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Passkey Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Passkey",
        "tags": [
          "Passkeys"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "patch": {
        "description": "Rename one of the calling developer's passkeys.\n\nOwner-scoped: a passkey id that isn't this developer's returns 404 with no\noracle distinguishing \"not yours\" from \"doesn't exist\".",
        "operationId": "rename_passkey_api_v1_passkeys__passkey_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "passkey_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Passkey Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenamePasskeyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Rename Passkey",
        "tags": [
          "Passkeys"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/pats": {
      "get": {
        "description": "List the caller's own PATs in an organisation, cursor-paginated.\n\nScope is per-developer: every org member sees only their own PATs,\nnever anyone else's. Includes the caller's revoked rows so they\nkeep an audit trail of their own credentials. Default page size 100,\nhard cap 1000.",
        "operationId": "list_pats_api_v1_pats_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Pats",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "post": {
        "description": "Create a PAT scoped to an organization.\n\nReturns the secret value in the response body \u2014 once. Subsequent\nGET / list responses never include it; if the developer loses it,\nthey create a new one and revoke the old one.",
        "operationId": "create_pat_api_v1_pats_post",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPat"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewlyCreatedPat"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Pat",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/pats/all": {
      "get": {
        "description": "List PATs in the organization, across all developers (admin-only).\n\nThe incident-response view: an admin investigating a suspected\ncredential leak uses this to enumerate PATs and pick which to revoke.\nPer-developer listing is still the default for non-admin members at\nGET /pats. Cursor-paginated \u2014 default page size 100, hard cap 1000.",
        "operationId": "list_all_pats_api_v1_pats_all_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List All Pats",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/pats/grantable-scopes": {
      "get": {
        "description": "Return the PAT scopes the caller may mint in an organisation.\n\nPowers the create UI's checkbox list: the same privilege ceiling the\ncreate route enforces, surfaced up front so the UI shows only the\nscopes the developer can actually grant rather than letting them pick one\nthat would 403. Authorised as org READ, exactly like create (a non-member\nis rejected by that gate); no secrets are involved, only the caller's own\ngrantable set.",
        "operationId": "get_grantable_scopes_api_v1_pats_grantable_scopes_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GrantableScopesResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Grantable Scopes",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/pats/{pat_id}": {
      "delete": {
        "description": "Revoke a PAT.\n\nOwners can always revoke their own PAT. Org admins can additionally\nrevoke any PAT in the org (incident-response cleanup). Other\nmembers get NOT_FOUND for PATs they don't own \u2014 same shape as a\ncross-org lookup so ownership can't be enumerated.",
        "operationId": "revoke_pat_api_v1_pats__pat_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "pat_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pat Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke Pat",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/pats/{pat_id}/permanent": {
      "delete": {
        "description": "Permanently delete a PAT (hard delete).\n\nDistinct from revoke (DELETE /pats/{id}), which is a soft stop that keeps\nthe row so the owner retains it in their list as an audit trail. This\nremoves the record and its token hash for good. A full snapshot is written to\nthe immutable audit trail first, so the deletion stays forensically\nreconstructable even though nothing of the PAT survives.\n\nOwners can delete their own PAT; org admins can additionally delete any\nPAT in the org (incident-response cleanup). Other members get NOT_FOUND\nfor PATs they don't own: the same shape as a cross-org lookup so\nownership can't be enumerated.",
        "operationId": "delete_pat_api_v1_pats__pat_id__permanent_delete",
        "parameters": [
          {
            "in": "path",
            "name": "pat_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pat Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Pat",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/pats/{pat_id}/rotate": {
      "post": {
        "description": "Replace an existing PAT atomically; carry name + scopes + expiry over.\n\nA developer can only rotate PATs they own. The new token comes back\nin the response body (one-shot, as with create); the old PAT is\nrevoked once the new one is live so the caller is never left without\na working credential.",
        "operationId": "rotate_pat_api_v1_pats__pat_id__rotate_post",
        "parameters": [
          {
            "in": "path",
            "name": "pat_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pat Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewlyCreatedPat"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Rotate Pat",
        "tags": [
          "PATs"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/platforms": {
      "get": {
        "description": "Return the canonical, ordered catalog of supported build platforms.\n\nPublic reference data: no authentication is required. This list is the single\nsource of truth for which platforms a build can target, and the order it is\nreturned in is the order to present it in. It changes only when AppGantry adds\nor retires platform support, so it is safe to fetch once and cache.",
        "operationId": "list_platforms_api_v1_platforms_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/PlatformInfo"
                  },
                  "title": "Response List Platforms Api V1 Platforms Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          }
        },
        "summary": "List the platforms a build can target (no auth)",
        "tags": [
          "Platforms"
        ]
      }
    },
    "/api/v1/project-access-tokens": {
      "get": {
        "description": "List the project's access tokens, newest first, cursor-paginated.\n\nRequires project MAINTAINER (or org MANAGER). Revoked rows are included so\nthe caller keeps an audit trail; the secret is never returned. Default page\nsize 100, hard cap 1000.",
        "operationId": "list_project_access_tokens_api_v1_project_access_tokens_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectAccessTokenPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Project Access Tokens",
        "tags": [
          "Project Access Tokens"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "post": {
        "description": "Mint a project access token scoped to a project.\n\nRequires project MAINTAINER (or org MANAGER). The requested\n``granted_role`` is capped at the caller's own effective project role by\nthe service-side privilege ceiling, so a Maintainer cannot grant ADMIN.\nReturns the secret value in the response body once; later list responses\nnever include it.",
        "operationId": "create_project_access_token_api_v1_project_access_tokens_post",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewProjectAccessToken"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewlyCreatedProjectAccessToken"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Project Access Token",
        "tags": [
          "Project Access Tokens"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/project-access-tokens/{token_id}": {
      "delete": {
        "description": "Revoke a project access token. Idempotent.\n\nRequires project MAINTAINER (or org MANAGER). A token id that doesn't belong\nto this (org, project) returns NOT_FOUND, the same shape a missing id\nproduces, so tokens can't be enumerated across projects.",
        "operationId": "revoke_project_access_token_api_v1_project_access_tokens__token_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "token_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Token Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke Project Access Token",
        "tags": [
          "Project Access Tokens"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/project-access-tokens/{token_id}/permanent": {
      "delete": {
        "description": "Permanently delete a project access token (hard delete). Idempotent.\n\nDistinct from revoke (DELETE /{token_id}), which is a soft stop that keeps\nthe record so it stays in the project's list as an audit trail. This removes\nthe record and its token hash for good, freeing the name for reuse. A full\nsnapshot is written to the immutable audit trail first, so the deletion stays\nforensically reconstructable even though nothing of the token survives.\n\nRequires project MAINTAINER (or org MANAGER), exactly like revoke. Idempotent:\na token id that doesn't belong to this (org, project), whether already\ndeleted or never present, returns 204, so a repeat call or a stale id is\nsafe and token existence can't be probed across projects.",
        "operationId": "delete_project_access_token_api_v1_project_access_tokens__token_id__permanent_delete",
        "parameters": [
          {
            "in": "path",
            "name": "token_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Token Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Project Access Token",
        "tags": [
          "Project Access Tokens"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/project-access-tokens/{token_id}/rotate": {
      "post": {
        "description": "Replace a token's secret atomically; keep its identity, name and role.\n\nRequires project MAINTAINER (or org MANAGER). Because rotation hands back a\nfresh working secret, the privilege ceiling is re-checked against the\ntoken's existing role, so a Maintainer cannot reissue an ADMIN-scoped\ntoken. The new secret comes back in the response body once; the old secret\nstops authenticating the moment the new one is live.",
        "operationId": "rotate_project_access_token_api_v1_project_access_tokens__token_id__rotate_post",
        "parameters": [
          {
            "in": "path",
            "name": "token_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Token Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewlyCreatedProjectAccessToken"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Rotate Project Access Token",
        "tags": [
          "Project Access Tokens"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/projects": {
      "get": {
        "description": "Get the projects in the organization.",
        "operationId": "get_projects_api_v1_projects_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Project"
                  },
                  "title": "Response Get Projects Api V1 Projects Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Projects",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Add a new project to the organisation's workspace.",
        "operationId": "create_project_api_v1_projects_post",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewProject"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "title": "Response Create Project Api V1 Projects Post",
                  "type": "string"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}": {
      "delete": {
        "description": "Delete the given project.",
        "operationId": "delete_project_api_v1_projects__project_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Project",
        "tags": [
          "Projects"
        ]
      },
      "get": {
        "description": "Get the info for the given project.",
        "operationId": "get_project_api_v1_projects__project_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Project",
        "tags": [
          "Projects"
        ]
      },
      "patch": {
        "description": "Update a project.",
        "operationId": "update_project_api_v1_projects__project_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchProject"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/icon": {
      "delete": {
        "description": "Remove the project's icon. ADMIN-gated; idempotent 204 when none set.",
        "operationId": "delete_project_icon_api_v1_projects__project_id__icon_delete",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete the project's icon",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Store the project's icon.\n\nADMIN-gated: a project's branding is an admin concern, like editing its name.\nThe upload body is capped at a fixed size limit (enforced by\nreading one byte past the cap, independent of any client Content-Length), and\nthe true image type is sniffed from the bytes server-side (the declared\nContent-Type is ignored). 204 on success; 400 for a non-image payload; 413\nwhen over the size cap.",
        "operationId": "set_project_icon_api_v1_projects__project_id__icon_post",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_set_project_icon_api_v1_projects__project_id__icon_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Upload (or replace) the project's icon",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/invites": {
      "get": {
        "description": "Return non-expired pending invites, newest first, cursor-paginated.\nProject ADMIN.",
        "operationId": "list_project_invites_api_v1_projects__project_id__invites_get",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInvitePage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List pending project invites",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Generate a project invite + (later) send the email. Project ADMIN.",
        "operationId": "create_project_invite_api_v1_projects__project_id__invites_post",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewProjectInvite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInvite"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Invite a developer to the project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/invites/{invite_id}": {
      "delete": {
        "description": "Revoke a pending project invite. Project ADMIN.",
        "operationId": "revoke_project_invite_api_v1_projects__project_id__invites__invite_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke a pending project invite",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/invites/{invite_id}/resend": {
      "post": {
        "description": "Re-send a pending project invite's email and rotate its token + 14-day\nwindow, keeping the invite id stable. Project ADMIN. Throttled per\ninvite; the response omits the raw token (it went out by email).",
        "operationId": "resend_project_invite_api_v1_projects__project_id__invites__invite_id__resend_post",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "invite_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Invite Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInvite"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Re-send a pending project invite",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/members": {
      "get": {
        "description": "Return the project's members with their roles, newest first, cursor-paginated. Was a bare array silently truncated at 1000; now the full set is\nreachable page by page via ``next_cursor``. Project READ.",
        "operationId": "list_project_members_api_v1_projects__project_id__members_get",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List project members",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/members/{developer_id}": {
      "delete": {
        "description": "Remove a project member. Project ADMIN; last-admin guarded.",
        "operationId": "remove_project_member_api_v1_projects__project_id__members__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove a member from a project",
        "tags": [
          "Projects"
        ]
      },
      "patch": {
        "description": "Change a member's project role. Project ADMIN; last-admin guarded.",
        "operationId": "change_project_member_role_api_v1_projects__project_id__members__developer_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeProjectMemberRole"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Change a project member's role",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/testers": {
      "get": {
        "description": "List project-tier testers \u2014 active + pending merged, newest first,\ncursor-paginated. Project ADMIN-only. Was a bare array truncated at\n1000; active and pending now interleave by timestamp.",
        "operationId": "list_project_testers_api_v1_projects__project_id__testers_get",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterMemberPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List project testers (active + pending)",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "description": "Direct-add a tester at project tier by email. Project ADMIN-only.",
        "operationId": "add_project_tester_api_v1_projects__project_id__testers_post",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddTesterRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Add a tester to the project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/testers/pending/{pending_id}": {
      "delete": {
        "description": "Cancel a pending project-tier tester invite.",
        "operationId": "remove_pending_project_tester_api_v1_projects__project_id__testers_pending__pending_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Cancel a pending project tester grant",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/testers/pending/{pending_id}/resend": {
      "post": {
        "description": "Re-send a pending project-tier tester grant's notification email, keeping\nthe pending-grant id stable so the WebApp's pending list stays consistent. ADMIN-only. Throttled per grant; a missing/already-accepted grant\nreturns a uniform 404 (anti-enumeration). No token is issued, so the response\nbody is empty.",
        "operationId": "resend_pending_project_tester_api_v1_projects__project_id__testers_pending__pending_id__resend_post",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pending_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Pending Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Re-send a pending project tester grant",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/projects/{project_id}/testers/{developer_id}": {
      "delete": {
        "description": "Revoke an active project-tier tester grant.",
        "operationId": "remove_project_tester_api_v1_projects__project_id__testers__developer_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Remove an active project tester",
        "tags": [
          "Projects"
        ]
      },
      "get": {
        "description": "Detail for one active project-tier tester, for the WebApp tester detail\npage. Project ADMIN-only, PAT READ scope (mirrors the list). Returns the\ntester's direct project grant plus their org-level tester groups. A\ndeveloper who is not a direct active project tester returns a uniform 404\n(anti-enumeration, matching the list/DELETE surfaces).",
        "operationId": "get_project_tester_api_v1_projects__project_id__testers__developer_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "developer_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Developer Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterDetailResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get a single project tester",
        "tags": [
          "Projects"
        ]
      }
    },
    "/api/v1/publish": {
      "get": {
        "description": "List a project's publish jobs, newest first, cursor-paginated.\n\nStore publishing is not operational, so a job listed here ends ``failed``\nrather than reaching a store.",
        "operationId": "list_publishes_api_v1_publish_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishRequestPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Publishes",
        "tags": [
          "Publish"
        ]
      },
      "post": {
        "description": "Create and track a publish job for a build against one of the project's\nconnections.\n\nStore publishing is not operational: the job is created and can be read\nback, but it ends ``failed`` and no artifact reaches the provider. Use App\nStore Connect or the Google Play Console for store releases today.\n\nIdempotent while a job is in flight: re-submitting the same (connection,\nbuild) collapses onto the existing job. Returns the publish job id.",
        "operationId": "create_publish_api_v1_publish_post",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPublishRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Publish",
        "tags": [
          "Publish"
        ]
      }
    },
    "/api/v1/publish/{publish_id}": {
      "get": {
        "description": "Get the status of a single publish job (scoped to the project).\n\nStore publishing is not operational, so a terminal job reports ``failed``;\n``completed`` belongs to the future status contract.",
        "operationId": "get_publish_api_v1_publish__publish_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "publish_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Publish Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishRequest"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Publish",
        "tags": [
          "Publish"
        ]
      }
    },
    "/api/v1/releases": {
      "get": {
        "description": "List releases on a channel, newest first, cursor-paginated.\n\nIncludes rolled-back releases \u2014 the rolled_back_at field flags\nwhich rows are currently effective. Each release row carries up\nto 10KB of release notes; an unpaginated list on a busy channel\ncould serialise a multi-megabyte response. Default page size 100,\nhard cap 1000.",
        "operationId": "get_releases_api_v1_releases_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleasePage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Releases",
        "tags": [
          "Releases"
        ]
      },
      "post": {
        "description": "Publish a build to a channel.",
        "operationId": "create_release_api_v1_releases_post",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewRelease"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/current": {
      "get": {
        "description": "Return the active release on a channel for a given platform.\n\n\"Active\" means the most recent release whose build isn't revoked\nand which itself isn't rolled back. Returns null if no such release.",
        "operationId": "get_current_release_api_v1_releases_current_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "platform",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ReleaseResponse"
                    },
                    {
                      "type": "null"
                    }
                  ],
                  "title": "Response Get Current Release Api V1 Releases Current Get"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Current Release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/current/all": {
      "get": {
        "description": "Return the active release per platform on a channel, in one query.\n\nSkips platforms with no current release entirely (no null entries).\nSaves six round-trips against the per-platform /current endpoint for\ndashboards or CI status checks. Bounded by Platform enum size (~6\nrows max) so no pagination here.",
        "operationId": "get_current_releases_all_platforms_api_v1_releases_current_all_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ReleaseResponse"
                  },
                  "title": "Response Get Current Releases All Platforms Api V1 Releases Current All Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Current Releases All Platforms",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}": {
      "delete": {
        "description": "Soft-delete a release, removing it from the list / current / download paths.\n\nThe row is preserved (history + audit). Idempotent: deleting an\nalready-deleted release is a 204 no-op.",
        "operationId": "delete_release_api_v1_releases__release_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Release",
        "tags": [
          "Releases"
        ]
      },
      "get": {
        "description": "Get a single release by ID, including rolled-back ones.",
        "operationId": "get_release_api_v1_releases__release_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/analytics": {
      "get": {
        "description": "Aggregate install counters for a release.\n\nTotal downloads, unique testers, and first/last download timestamps for the\nrelease's build. No tester PII, so READ access suffices.",
        "operationId": "get_release_analytics_api_v1_releases__release_id__analytics_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseAnalytics"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Install/download analytics for a release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/disable": {
      "post": {
        "description": "Disable a release so it is no longer served as current or downloadable.\n\nThe row is preserved and can be re-enabled. Idempotent: disabling an\nalready-disabled release is a 204 no-op.",
        "operationId": "disable_release_api_v1_releases__release_id__disable_post",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Disable Release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/enable": {
      "post": {
        "description": "Enable a release so it is served as current and downloadable again.\n\nIdempotent: enabling an already-enabled release is a 204 no-op.",
        "operationId": "enable_release_api_v1_releases__release_id__enable_post",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Enable Release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/install-status": {
      "get": {
        "description": "Per-tester install status for a release's distribution group.\n\nLists the channel's DIRECT testers annotated with whether/when they've\ninstalled the release's build. Reveals tester emails, so ADMIN-gated to\nmatch the channel-tester listing.",
        "operationId": "get_release_install_status_api_v1_releases__release_id__install_status_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterInstallStatusPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Per-tester install status for a release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/mandatory": {
      "post": {
        "description": "Mark a release as a mandatory (required) update.\n\nClients reading the current release should force the update. Idempotent:\nmarking an already-mandatory release is a 204 no-op.",
        "operationId": "mark_release_mandatory_api_v1_releases__release_id__mandatory_post",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Mark Release Mandatory",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/optional": {
      "post": {
        "description": "Relax a release back to an optional update.\n\nIdempotent: marking an already-optional release is a 204 no-op.",
        "operationId": "mark_release_optional_api_v1_releases__release_id__optional_post",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Mark Release Optional",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/releases/{release_id}/rollback": {
      "post": {
        "description": "Roll back a release. The release row is preserved with rolled_back_at set.",
        "operationId": "roll_back_release_api_v1_releases__release_id__rollback_post",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Roll Back Release",
        "tags": [
          "Releases"
        ]
      }
    },
    "/api/v1/sdk-apps": {
      "get": {
        "description": "List the SDK apps bound to a channel, newest first, cursor-paginated.\n\nNever exposes the secret \u2014 only its SHA-256 persists and that is never\nreturned. Default page size 100, hard cap 1000.",
        "operationId": "list_sdk_apps_api_v1_sdk_apps_get",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkAppPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Sdk Apps",
        "tags": [
          "SDK Apps"
        ]
      },
      "post": {
        "description": "Create an SDK app credential bound to a channel.\n\nThe response carries the raw ``app_secret`` exactly once \u2014 the only path\nthrough which it is ever exposed. If lost, create a new app and revoke this\none.",
        "operationId": "create_sdk_app_api_v1_sdk_apps_post",
        "parameters": [
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewSdkApp"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewlyCreatedSdkApp"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Sdk App",
        "tags": [
          "SDK Apps"
        ]
      }
    },
    "/api/v1/sdk-apps/{app_id}": {
      "delete": {
        "description": "Revoke an SDK app. Idempotent; disables its installs' update tokens.\n\nRevoking the app cascades-disables enroll / refresh for every install bound\nto it (enforced via the app-active JOIN in the install lookup), so a leaked\napp secret is contained by one revocation.",
        "operationId": "revoke_sdk_app_api_v1_sdk_apps__app_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "app_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "App Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke Sdk App",
        "tags": [
          "SDK Apps"
        ]
      }
    },
    "/api/v1/sessions": {
      "delete": {
        "description": "Revoke every one of the calling developer's sessions (\"log out all devices\").\n\nRevokes all refresh tokens AND raises the account's minimum-issued-at cutoff\nso outstanding access tokens stop validating fleet-wide within a short\npropagation window (about 60 s). This includes the caller's own current\nsession, so after this call the client should discard its credentials and\nre-authenticate.",
        "operationId": "revoke_all_sessions_api_v1_sessions_delete",
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke All Sessions",
        "tags": [
          "Sessions"
        ],
        "x-appgantry-auth": "jwt-only"
      },
      "get": {
        "description": "List the calling developer's active sessions.\n\nReturns non-revoked, unexpired sessions (the ones that could still mint a\nfresh access JWT), most-recently-used first. The refresh-token hash is\nnever included.",
        "operationId": "list_sessions_api_v1_sessions_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/SessionResponse"
                  },
                  "title": "Response List Sessions Api V1 Sessions Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Sessions",
        "tags": [
          "Sessions"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/sessions/{session_id}": {
      "delete": {
        "description": "Revoke one of the calling developer's sessions.\n\nOwner-scoped: a session id that isn't an active session of this developer\n(unknown, already revoked, or another developer's) returns 404 with no\noracle distinguishing the cases. The revoked device can no longer refresh;\nits current access JWT keeps working until it expires (a few minutes),\nsince access JWTs aren't per-session. To cut every device immediately,\nuse the revoke-all endpoint (which also invalidates outstanding JWTs).",
        "operationId": "revoke_session_api_v1_sessions__session_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "session_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Session Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor; the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Revoke Session",
        "tags": [
          "Sessions"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/store-connections": {
      "get": {
        "description": "List a project's store connections, newest first, cursor-paginated.\nSecret material is never included.",
        "operationId": "list_store_connections_api_v1_store_connections_get",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "title": "Limit",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Cursor"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreConnectionPage"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List Store Connections",
        "tags": [
          "Store Connections"
        ]
      },
      "post": {
        "description": "Create or replace the project's connection for a provider.\n\nOne connection exists per (project, provider); re-submitting the same\nprovider replaces its credentials (a rotation). Returns the connection id.",
        "operationId": "create_store_connection_api_v1_store_connections_post",
        "parameters": [
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewStoreConnection"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Create Store Connection",
        "tags": [
          "Store Connections"
        ]
      }
    },
    "/api/v1/store-connections/{connection_id}": {
      "delete": {
        "description": "Delete a project's store connection.",
        "operationId": "delete_store_connection_api_v1_store_connections__connection_id__delete",
        "parameters": [
          {
            "in": "path",
            "name": "connection_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Connection Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Delete Store Connection",
        "tags": [
          "Store Connections"
        ]
      },
      "get": {
        "description": "Get a single store connection (no secret material).",
        "operationId": "get_store_connection_api_v1_store_connections__connection_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "connection_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Connection Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreConnection"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get Store Connection",
        "tags": [
          "Store Connections"
        ]
      },
      "patch": {
        "description": "Relabel a connection and/or rotate its credentials. Provider is immutable.",
        "operationId": "update_store_connection_api_v1_store_connections__connection_id__patch",
        "parameters": [
          {
            "in": "path",
            "name": "connection_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Connection Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchStoreConnection"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreConnection"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Update Store Connection",
        "tags": [
          "Store Connections"
        ]
      }
    },
    "/api/v1/store-connections/{connection_id}/register-devices": {
      "post": {
        "description": "Sync the org's Apple-family devices to this App Store Connect account.\n\nReconciles the UDIDs collected by the device registry against the\nconnection's ASC account and registers the missing ones, so an ad-hoc build\ncan install on them. EDIT_PROJECT because it spends the project's ASC\ncredential. Per-device failures are reported in the result, not raised; only\na failure that aborts the whole sync (unknown connection, wrong provider,\nunreadable credential, or ASC unreachable) returns an error status.",
        "operationId": "register_devices_api_v1_store_connections__connection_id__register_devices_post",
        "parameters": [
          {
            "in": "path",
            "name": "connection_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Connection Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "project_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Project Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "organization_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Organization Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceRegistrationResult"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Payment required. Returned when the organization's writes are locked for billing: its subscription payment failed, its trial ended, or it has committed to paying but has no card on file yet. The specific reason is in `details.lock_reason`."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a token that lacks the scope this route requires; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not found. Returned when the resource does not exist, or the caller has no access to it \u2014 the two are deliberately indistinguishable so the route cannot be used to enumerate resources."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Register Devices",
        "tags": [
          "Store Connections"
        ]
      }
    },
    "/api/v1/testers/invites/accept": {
      "post": {
        "description": "Accept the invite as the logged-in developer.\n\nSingle-use: once accepted_at is set on the invite row, no other\naccepter wins. ``require_no_pat`` refuses machine-token accepts.",
        "operationId": "accept_channel_invite_api_v1_testers_invites_accept_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AcceptChannelInviteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcceptChannelInviteResponse"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when any of: the request authenticated with a Personal Access Token or a project access token, and this route accepts only a logged-in developer; the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Accept a channel tester invite",
        "tags": [
          "Testers"
        ],
        "x-appgantry-auth": "jwt-only"
      }
    },
    "/api/v1/testers/invites/{invite_token}/preview": {
      "get": {
        "description": "Resolve the names + expiry for the interstitial UI. No auth.\n\nNOT_FOUND on expired / used / unknown collapses to one shape so a\nstolen token can't probe channel names \u2014 the response only\nsucceeds for live, unused, unexpired tokens.",
        "operationId": "preview_channel_invite_api_v1_testers_invites__invite_token__preview_get",
        "parameters": [
          {
            "in": "path",
            "name": "invite_token",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 1,
              "title": "Invite Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelInvitePreview"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Preview a channel tester invite (interstitial)",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/builds/{build_id}/download": {
      "get": {
        "description": "Stream a build the tester has access to.\n\nAuthorisation: the developer must hold an any-tier tester grant\nAND the build must be the current release of a channel that\ngrant covers. Builds that have been revoked or rolled back\nreturn NOT_FOUND.",
        "operationId": "my_tester_download_api_v1_testers_me_builds__build_id__download_get",
        "parameters": [
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Download a build the caller has tester access to",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/channels": {
      "get": {
        "description": "List every channel the caller can install from, annotated with which\ntier of grant gives them access.",
        "operationId": "list_my_tester_channels_api_v1_testers_me_channels_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/TesterChannelSummary"
                  },
                  "title": "Response List My Tester Channels Api V1 Testers Me Channels Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List channels the caller has tester access to (with tier annotation)",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/channels/{channel_id}/notification-preference": {
      "get": {
        "description": "Return whether the caller has muted release notifications for the channel.\n\nNOT_FOUND if the caller has no grant on the channel (anti-enumeration).",
        "operationId": "get_my_channel_notification_preference_api_v1_testers_me_channels__channel_id__notification_preference_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelNotificationPreference"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Get the caller's release-notification preference for a channel",
        "tags": [
          "Testers"
        ]
      },
      "put": {
        "description": "Mute or unmute release notifications for the channel for the caller.\n\nNOT_FOUND if the caller has no grant on the channel (anti-enumeration).\nIdempotent.",
        "operationId": "set_my_channel_notification_preference_api_v1_testers_me_channels__channel_id__notification_preference_put",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChannelNotificationPreference"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelNotificationPreference"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Set the caller's release-notification preference for a channel",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/channels/{channel_id}/releases/current": {
      "get": {
        "description": "Return the per-platform current release on the channel.\n\nAccess is resolved via any tier of grant. NOT_FOUND if the caller\nhas no grant \u2014 same shape as a non-existent channel so listing\ncan't probe channels the caller doesn't reach.",
        "operationId": "my_tester_current_releases_api_v1_testers_me_channels__channel_id__releases_current_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List current releases on a channel (per platform)",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/organizations": {
      "get": {
        "description": "Return the org IDs the caller has tester access to via any tier.",
        "operationId": "list_my_tester_orgs_api_v1_testers_me_organizations_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List organisations the caller has tester access to",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/projects": {
      "get": {
        "description": "Return (org_id, project_id) tuples the caller has tester access to.",
        "operationId": "list_my_tester_projects_api_v1_testers_me_projects_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List projects the caller has tester access to",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/releases": {
      "get": {
        "description": "List every currently-installable release across the caller's channels.\n\nOne item per (channel, platform) that has a current release, annotated\nwith the access tier, app display metadata (version, notes, size) and a\nrelative ``install_url`` for the tester download route. Rolled-back,\ndisabled and soft-deleted releases \u2014 and revoked builds \u2014 are omitted.",
        "operationId": "list_my_tester_releases_api_v1_testers_me_releases_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/TesterReleaseListItem"
                  },
                  "title": "Response List My Tester Releases Api V1 Testers Me Releases Get",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "List all releases the caller can install (with install metadata)",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/releases/{release_id}/android-install-link": {
      "get": {
        "description": "Mint a tokenised session-less .apk install link for a release.\n\nThe Android counterpart to ``ios-install-link``, at parity with it: gated by\nthe caller's tester grant on the release's channel, it returns a short-lived\ncapability-token URL the device fetches on its own. Android has no manifest\nindirection, so ``install_url`` points straight at the platform-agnostic OTA\ndownload route (``/public/v1/ota/...``) \u2014 the browser hands the .apk to the\npackage installer.\n\n404 for an unknown release, a release the caller has no tester grant for, a\nrevoked build, or a non-Android build \u2014 all indistinguishable so the endpoint\ncan't probe release state.",
        "operationId": "my_tester_android_install_link_api_v1_testers_me_releases__release_id__android_install_link_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterAndroidInstallLink"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Mint an Android direct-install link for a release the caller can install",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/testers/me/releases/{release_id}/ios-install-link": {
      "get": {
        "description": "Mint a tokenised ``itms-services://`` OTA install link for a release.\n\nThe authenticated, private-channel counterpart to the public install\nmanifest: iOS can't carry the tester's session when Springboard\nfetches the manifest + .ipa, so this endpoint \u2014 gated by the caller's tester\ngrant on the release's channel \u2014 returns a short-lived capability-token link\nthe device can use on its own. The link points at the unauthenticated OTA\nmanifest route (``/public/v1/ota/...``), which validates the token.\n\n404 for an unknown release, a release the caller has no tester grant for, a\nrevoked build, or a non-iOS build / one without a bundle_identifier \u2014 all\nindistinguishable so the endpoint can't probe release state.",
        "operationId": "my_tester_ios_install_link_api_v1_testers_me_releases__release_id__ios_install_link_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterIosInstallLink"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Not authenticated. Returned when the request carries no credential, or its token is malformed, expired, revoked, issued before a logout, or belongs to a session that has been ended."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Forbidden. Returned when the caller's organization requires two-factor authentication, its grace window has elapsed, and the caller has not enrolled a factor."
          }
        },
        "security": [
          {
            "AppGantryAuthBase": []
          }
        ],
        "summary": "Mint an iOS OTA install link for a release the caller can install",
        "tags": [
          "Testers"
        ]
      }
    },
    "/api/v1/version": {
      "get": {
        "description": "Return the deployed build identifier (git SHA / image tag) + environment.\n\nLets a caller confirm which revision is running so deploy drift is\ndetectable from outside. ``build`` is null when the deploy pipeline hasn't\nset a release identifier (e.g. local dev).",
        "operationId": "get_version_api_v1_version_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionResponse"
                }
              }
            },
            "description": "Successful Response"
          }
        },
        "summary": "Report the running build's identifier and environment (no auth)",
        "tags": [
          "Miscellaneous"
        ]
      }
    },
    "/public/v1/channels/{channel_id}/builds/{build_id}/download": {
      "get": {
        "description": "Stream a build that is currently live on a public channel.\n\nAuthorisation is the channel's ``public`` flag plus the build being the\ncurrent release of that channel \u2014 there is no caller identity. A revoked\nbuild, a build not live on the channel, or a private channel all return 404.",
        "operationId": "public_download_public_v1_channels__channel_id__builds__build_id__download_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "build_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Build Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Download a build from a public channel (no auth)",
        "tags": [
          "Public"
        ]
      }
    },
    "/public/v1/channels/{channel_id}/manifest.plist": {
      "get": {
        "description": "Serve the OTA install manifest for a public channel's current release.\n\niOS/visionOS fetches this plist over plain HTTPS with no auth header (the\ndevice, not the user's session, makes the request), which is why it lives on\nthe public surface. A tester taps an ``itms-services://?action=download-manifest\n&url=<absolute manifest url>`` link; the OS reads this plist and installs the\nreferenced .ipa. ``platform`` selects which of the channel's current releases\nto serve and defaults to iOS; only OTA-installable platforms (iOS, visionOS)\nare accepted.\n\n404 (NOT_FOUND) when the channel isn't public, has no current release for the\nplatform, the platform isn't OTA-installable, or the build carries no\nbundle_identifier \u2014 uniform with the rest of the public surface so a caller\ncan't probe channel state.",
        "operationId": "public_ios_manifest_public_v1_channels__channel_id__manifest_plist_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "platform",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Platform",
              "default": "ios"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "itms-services OTA install manifest for a public channel (no auth)",
        "tags": [
          "Public"
        ]
      }
    },
    "/public/v1/channels/{channel_id}/releases/current": {
      "get": {
        "description": "Resolve the current installable release of a public channel.\n\n404 for a private/unknown channel or one with no live release for the\nplatform \u2014 indistinguishable on purpose.",
        "operationId": "public_current_release_public_v1_channels__channel_id__releases_current_get",
        "parameters": [
          {
            "in": "path",
            "name": "channel_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Channel Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "platform",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicReleaseInfo"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Get the current release of a public channel (no auth)",
        "tags": [
          "Public"
        ]
      }
    },
    "/public/v1/devices/enroll/{token}": {
      "get": {
        "description": "Serve the browser landing page a tester lands on when opening the link.\n\nThe tester opens this URL in Safari on the device to be enrolled. Since iOS\n12.2, navigating Safari *directly* at a ``.mobileconfig`` hands the file to\nthe profile installer and leaves a blank page, with no auto-prompt and no\ninstruction to finish the install in Settings -- which is exactly the \"blank\nwhite screen, nothing happened\" symptom. So this route no longer\nreturns the raw profile; it returns an HTML page that explains the flow,\nlinks to the actual profile download (``.../{token}/profile``), and tells the\nuser to complete the install under Settings. In-app browsers (Mail, Slack,\nMessages) can't install profiles at all, so the page also tells the user to\nopen the link in Safari.\n\nAn invalid or expired token renders a canonical error page (still HTTP 404)\nso the surface stays uniform and non-enumerable, matching the profile and\ncallback routes.",
        "operationId": "serve_device_enrollment_landing_public_v1_devices_enroll__token__get",
        "parameters": [
          {
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "title": "Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Human-facing landing page for an iOS device-enrollment link",
        "tags": [
          "Devices"
        ]
      }
    },
    "/public/v1/devices/enroll/{token}/callback": {
      "post": {
        "description": "Persist the device that POSTed its attributes back from the profile.\n\nThe body is Apple's PKCS#7-signed attributes plist. The capability token in\nthe URL is the trust anchor (signature verification is not performed yet); the\nservice extracts the embedded plist, validates the UDID, and upserts the\ndevice into the org registry.",
        "operationId": "device_enrollment_callback_public_v1_devices_enroll__token__callback_post",
        "parameters": [
          {
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "title": "Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Receive a device's attributes and register it (Apple Profile Service callback)",
        "tags": [
          "Devices"
        ]
      }
    },
    "/public/v1/devices/enroll/{token}/profile": {
      "get": {
        "description": "Serve the configuration profile the iOS device installs to report its UDID.\n\nReached from the landing page's install link. The device installs the\nreturned ``.mobileconfig`` and the OS POSTs its attributes back to the\ncallback URL embedded in the profile (which carries the same capability\ntoken).",
        "operationId": "serve_device_enrollment_profile_public_v1_devices_enroll__token__profile_get",
        "parameters": [
          {
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "title": "Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Serve the Apple Profile Service .mobileconfig for device enrollment",
        "tags": [
          "Devices"
        ]
      }
    },
    "/public/v1/ota/releases/{release_id}/download": {
      "get": {
        "description": "Stream a release's binary, authorised by an OTA capability token.\n\niOS follows the itms-services manifest's asset URL with no auth header, so\nthe URL carries the short-lived signed capability token. After validating it\nbinds this release, the billing gate, anonymous download record, and\nsigned-URL mint run exactly as on the public / tester / SDK download paths.\nA revoked build or non-installable release returns 404.",
        "operationId": "ota_download_public_v1_ota_releases__release_id__download_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "description": "Short-lived OTA capability token bound to this release",
            "in": "query",
            "name": "token",
            "required": true,
            "schema": {
              "description": "Short-lived OTA capability token bound to this release",
              "title": "Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Download a release binary via OTA capability token (no session)",
        "tags": [
          "Public"
        ]
      }
    },
    "/public/v1/ota/releases/{release_id}/manifest.plist": {
      "get": {
        "description": "Serve the iOS OTA install manifest for a release, authorised by a token.\n\niOS fetches this plist over plain HTTPS with no session \u2014 the device, not\nthe tester's browser, makes the request \u2014 so the URL carries a short-lived\nsigned capability token bound to this exact release (minted by\n``GET /testers/me/releases/{id}/ios-install-link``). The .ipa asset URL\nembedded in the plist points back at the OTA download route below with a\nfreshly minted token so the remaining TTL covers the binary fetch.\n\n404 on any invalid/expired/mismatched token or a non-installable release \u2014\nuniform with the rest of the public surface.",
        "operationId": "ota_ios_manifest_public_v1_ota_releases__release_id__manifest_plist_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "description": "Short-lived OTA capability token bound to this release",
            "in": "query",
            "name": "token",
            "required": true,
            "schema": {
              "description": "Short-lived OTA capability token bound to this release",
              "title": "Token",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "iOS itms-services OTA install manifest for a release (capability token)",
        "tags": [
          "Public"
        ]
      }
    },
    "/sdk/v1/apps/{app_key}": {
      "get": {
        "description": "Return app display metadata for the SDK update prompt.\n\nPresents the per-install update token as ``Authorization: Bearer <token>``.\nReturns the app name plus the current release's version / minimum-OS for the\nrequested platform and a relative ``icon_url`` (fetched from the companion\nicon route). A missing / malformed header or any unusable token collapses to\na uniform 401.",
        "operationId": "get_app_metadata_sdk_v1_apps__app_key__get",
        "parameters": [
          {
            "in": "path",
            "name": "app_key",
            "required": true,
            "schema": {
              "title": "App Key",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "platform",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          },
          {
            "in": "header",
            "name": "authorization",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Authorization"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkAppMetadata"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Get App Metadata",
        "tags": [
          "SDK"
        ]
      }
    },
    "/sdk/v1/apps/{app_key}/enroll": {
      "post": {
        "description": "Enroll an install: validate app_key + secret, mint a scoped update token.\n\nThe returned ``update_token`` is shown exactly once \u2014 the SDK stores it and\npresents it on subsequent calls. Every failure collapses to a uniform 401.",
        "operationId": "enroll_sdk_v1_apps__app_key__enroll_post",
        "parameters": [
          {
            "in": "path",
            "name": "app_key",
            "required": true,
            "schema": {
              "title": "App Key",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkEnrollRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkInstallToken"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Enroll",
        "tags": [
          "SDK"
        ]
      }
    },
    "/sdk/v1/apps/{app_key}/updates/latest": {
      "get": {
        "description": "Poll for a newer build, presenting the install update token as ``Bearer``.\n\nReturns the latest release's metadata, whether an update is available / the\ninstall is already current, and the mandatory-update signal, plus the\nrelative URLs the SDK uses to fetch the binary / iOS OTA manifest.\n\n``current_version`` is accepted for diagnostics/telemetry but the\nauthoritative comparison is on the monotonic ``current_build``. A missing /\nmalformed header or any unusable token collapses to a uniform 401.",
        "operationId": "check_for_update_sdk_v1_apps__app_key__updates_latest_get",
        "parameters": [
          {
            "in": "path",
            "name": "app_key",
            "required": true,
            "schema": {
              "title": "App Key",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "platform",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          },
          {
            "in": "query",
            "name": "current_version",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Current Version"
            }
          },
          {
            "in": "query",
            "name": "current_build",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Current Build"
            }
          },
          {
            "in": "header",
            "name": "authorization",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Authorization"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkUpdateInfo"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Check For Update",
        "tags": [
          "SDK"
        ]
      }
    },
    "/sdk/v1/releases/{release_id}/download": {
      "get": {
        "description": "302 to a short-lived signed URL for a release's build (Android APK / iOS .ipa).\n\nTwo authorisation paths converge here:\n\n* ``Authorization: Bearer <update_token>`` \u2014 the SDK fetching the binary\n  directly. Resolves install -> app -> channel; the release must be live on\n  that channel.\n* ``?token=<capability>`` \u2014 the iOS OTA path. iOS Springboard follows the\n  itms-services manifest's asset URL with no auth header, so the URL carries\n  a short-lived signed capability token bound to this release.\n\nA bad/missing credential is a uniform 401; a release that isn't installable is\na 404. Then the billing gate, anonymous download record, and signed-URL mint\nrun exactly as on the tester / public download paths.",
        "operationId": "download_sdk_v1_releases__release_id__download_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "token",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Token"
            }
          },
          {
            "in": "header",
            "name": "authorization",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Authorization"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Download a release binary (SDK bearer token, or iOS OTA capability)",
        "tags": [
          "SDK"
        ]
      }
    },
    "/sdk/v1/releases/{release_id}/install-reports": {
      "post": {
        "description": "Record a downloading/installing/installed/failed report for the caller's install.\n\nPresents the per-install update token as ``Authorization: Bearer <token>``.\nThe report is bound to a release on the caller's channel; the build number is\nderived server-side from that release. A missing/unusable token collapses to a\nuniform 401; a release that isn't on the caller's channel is a 404. On success\nthe body is empty with 202 (fire-and-forget telemetry).",
        "operationId": "report_install_sdk_v1_releases__release_id__install_reports_post",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "authorization",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Authorization"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkInstallReportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Report an install/update state for a release (SDK bearer token)",
        "tags": [
          "SDK"
        ]
      }
    },
    "/sdk/v1/releases/{release_id}/manifest.plist": {
      "get": {
        "description": "Serve the iOS OTA install manifest for a release.\n\niOS Springboard fetches this plist with no auth header, so the URL carries a\nshort-lived signed capability token (minted by the check-for-update poll)\nbound to this exact release. A missing/invalid/mismatched token is a uniform\n401; a release that isn't a live iOS build with a bundle_identifier is a 404.\n\nThe .ipa asset URL embedded in the plist points back at the download route\nabove with a freshly minted capability token, so iOS can fetch the binary\n(again with no header) through the same billing-gated path.",
        "operationId": "ios_manifest_sdk_v1_releases__release_id__manifest_plist_get",
        "parameters": [
          {
            "in": "path",
            "name": "release_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Release Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "token",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Token"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "iOS itms-services OTA install manifest for a release (capability token)",
        "tags": [
          "SDK"
        ]
      }
    },
    "/sdk/v1/token/refresh": {
      "post": {
        "description": "Rotate the caller's update token, presented as ``Authorization: Bearer``.\n\nA malformed / missing header is indistinguishable from an unusable token: both\nreturn the same uniform 401, so the surface can't be probed for token state.",
        "operationId": "refresh_sdk_v1_token_refresh_post",
        "parameters": [
          {
            "in": "header",
            "name": "authorization",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Authorization"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkInstallToken"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                }
              }
            },
            "description": "Bad request. The body is the flat error envelope, and `error` names the rule that refused it: `invalid_input` for a value the route parsed but would not accept, `operation_not_allowed` for a request that is not legal in the resource's current state, or `quota_exceeded` for a structural count limit.\n\nRequest validation failed: a field is missing, malformed, or out of range. The body is the flat error envelope with `error: \"validation_error\"`, and `details.errors` lists one entry per failing field, each carrying only `type`, `loc` and `msg`. Retrying the same request unchanged will fail the same way."
          }
        },
        "summary": "Refresh",
        "tags": [
          "SDK"
        ]
      }
    }
  },
  "servers": [
    {
      "description": "Staging (non-production)",
      "url": "https://api.appgantrystaging.com"
    }
  ],
  "tags": [
    {
      "description": "Sign in, refresh a session, sign out, and complete SSO or passkey authentication.",
      "name": "Login"
    },
    {
      "description": "List and revoke the developer's own active sessions.",
      "name": "Sessions"
    },
    {
      "description": "Enrol, confirm, disable and recover two-factor authentication for the calling developer.",
      "name": "MFA"
    },
    {
      "description": "Register and manage WebAuthn passkeys for passwordless sign-in.",
      "name": "Passkeys"
    },
    {
      "description": "The calling developer's own account: profile, email verification, password, and deletion.",
      "name": "Developers"
    },
    {
      "description": "Organizations and everything scoped to one: members and invites, roles, billing, usage, tier, SAML SSO configuration, storage-account configuration, and feature flags.",
      "name": "Organizations"
    },
    {
      "description": "Projects within an organization, their members, and their settings.",
      "name": "Projects"
    },
    {
      "description": "Upload, list, download, edit, revoke and delete build artifacts.",
      "name": "Builds"
    },
    {
      "description": "Distribution channels, the testers granted to them, and their invites.",
      "name": "Channels"
    },
    {
      "description": "Promote a build to a channel, roll back, and read release analytics.",
      "name": "Releases"
    },
    {
      "description": "Submit a build to an app store through a configured store connection, and read the resulting job. **Publishing is not operational yet:** no store adapter is implemented, so a submitted job is queued, claimed by the dispatcher, and recorded as `failed` without the artifact ever reaching Apple, Google or Microsoft. Ship builds to testers through channels and releases until this is live.",
      "name": "Publish"
    },
    {
      "description": "Store credentials an organization holds for App Store Connect, Google Play and Microsoft Intune. Credentials are validated against the provider when saved and are never returned on a response. They exist for the publishing pipeline, which is not operational yet: no store adapter is implemented, so a publish job submitted against a valid connection is recorded as failed without the artifact reaching the provider.",
      "name": "Store Connections"
    },
    {
      "description": "Testers, tester groups, their grants, and the tester-facing install surface.",
      "name": "Testers"
    },
    {
      "description": "Devices registered for ad-hoc distribution.",
      "name": "Devices"
    },
    {
      "description": "Applications registered for the in-app update SDK.",
      "name": "SDK Apps"
    },
    {
      "description": "The device surface the in-app update SDK calls. No developer credential \u2014 no JWT, no Personal Access Token, no project access token \u2014 is ever sent here. Most operations authenticate with the per-install update token that enrolment mints: it is presented as `Authorization: Bearer <token>`. Two are exceptions, because the caller has no token yet or cannot send a header at all: enrolment exchanges the app key and app secret for the update token, and the iOS install manifest is fetched by the operating system with a short-lived signed capability token in the query string.",
      "name": "SDK"
    },
    {
      "description": "The unauthenticated distribution surface a public channel's install link resolves through.",
      "name": "Public"
    },
    {
      "description": "Personal Access Tokens: long-lived, organization-bound integration credentials.",
      "name": "PATs"
    },
    {
      "description": "Project-bound tokens for CI that should not carry a human's identity.",
      "name": "Project Access Tokens"
    },
    {
      "description": "Outbound webhook subscriptions and their delivery signing secrets.",
      "name": "Webhooks"
    },
    {
      "description": "The organization and per-developer audit trails.",
      "name": "Audit"
    },
    {
      "description": "Read the feature flags in effect for an organization.",
      "name": "Feature Flags"
    },
    {
      "description": "The platforms AppGantry can distribute to.",
      "name": "Platforms"
    },
    {
      "description": "Service metadata and other endpoints that belong to no resource family.",
      "name": "Miscellaneous"
    }
  ]
}
