{
  "openapi": "3.0.0",
  "info": {
    "title": "Propper Organization API",
    "version": "1.0.0",
    "description": "Organization-scoped management API for the Propper platform.\n\n## Base URL\n\n- **Production:** `https://api.propper.ai/v1/org`\n\n## Authentication\n\nOAuth 2.0 Bearer token. The organization is resolved from the token, not a path parameter:\n\n```\nAuthorization: Bearer <access_token>\n```\n\n## OAuth Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `org:read` | Read the organization profile |\n| `users:read` | Read organization membership, member capabilities, and member preferences |\n| `users:write` | Invite members, change roles, remove members, cancel invitations, write member preferences |\n\nAll three scopes can be assigned to client applications you create and manage\nin your organization settings. `users:write` does not imply `users:read`:\nrequest both if your integration reads and writes.\n\nThe entitlement verdict projection (`view=salesforce`) is platform-only: it is\ncallable exclusively by Propper-generated Salesforce client applications and is\nnot reachable with any customer-requestable scope."
  },
  "servers": [
    {
      "url": "https://api.propper.ai",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Organization",
      "description": "The organization profile and its members"
    },
    {
      "name": "Members",
      "description": "Membership lifecycle: invitations, roles, and removal"
    },
    {
      "name": "Entitlements",
      "description": "The organization's derived entitlement verdict"
    }
  ],
  "paths": {
    "/v1/org": {
      "get": {
        "operationId": "getOrganization",
        "summary": "Get the organization profile",
        "description": "Returns the calling organization's profile. The organization is taken from the access token; there is no organization path parameter. Requires the `org:read` scope.",
        "tags": [
          "Organization"
        ],
        "responses": {
          "200": {
            "description": "The organization profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationProfile"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `org:read` scope."
          },
          "404": {
            "description": "The organization no longer exists."
          }
        }
      }
    },
    "/v1/org/members": {
      "get": {
        "operationId": "listOrganizationMembers",
        "summary": "List organization members",
        "description": "Returns the calling organization's members, newest first, paginated. The organization is taken from the access token. Requires the `users:read` scope.",
        "tags": [
          "Organization"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Defaults to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Defaults to 50; maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the membership roster.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationMemberList"
                }
              }
            }
          },
          "400": {
            "description": "A pagination parameter is malformed or out of range."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:read` scope."
          }
        }
      }
    },
    "/v1/org/members/invite": {
      "post": {
        "operationId": "inviteOrganizationMember",
        "summary": "Invite a member",
        "description": "Invites an email address to join the calling organization and sends the invitation email. The organization is taken from the access token. The invitation is valid for 7 days; re-inviting an address whose invitation has lapsed refreshes it in place. Requires the `users:write` scope.",
        "tags": [
          "Members"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberInviteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The invitation was created and sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invitation"
                }
              }
            }
          },
          "400": {
            "description": "The email address or role is missing or not accepted."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:write` scope."
          },
          "409": {
            "description": "The address already belongs to a member, already has a pending invitation, or the organization has no administrator to attribute the invitation to."
          }
        }
      }
    },
    "/v1/org/members/{userId}": {
      "put": {
        "operationId": "updateOrganizationMemberRole",
        "summary": "Change a member's role",
        "description": "Sets the role of one member of the calling organization. Members are addressed by user identifier, the same value the roster returns as `user.id`. A member of another organization is reported as not found. The last remaining administrator cannot be demoted. Requires the `users:write` scope.",
        "tags": [
          "Members"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's user identifier.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberRoleUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationMember"
                }
              }
            }
          },
          "400": {
            "description": "The role is missing or not one of the assignable roles."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:write` scope."
          },
          "404": {
            "description": "No such member in the calling organization."
          },
          "409": {
            "description": "The change would leave the organization without an administrator."
          }
        }
      },
      "delete": {
        "operationId": "removeOrganizationMember",
        "summary": "Remove a member",
        "description": "Removes one member from the calling organization and ends their active sessions for it. Sessions in other organizations the person belongs to are unaffected. The last remaining administrator cannot be removed. Requires the `users:write` scope.",
        "tags": [
          "Members"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's user identifier.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The member was removed."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:write` scope."
          },
          "404": {
            "description": "No such member in the calling organization."
          },
          "409": {
            "description": "The removal would leave the organization without an administrator."
          }
        }
      }
    },
    "/v1/org/members/{userId}/capabilities": {
      "get": {
        "operationId": "getOrganizationMemberCapabilities",
        "summary": "Get a member's capabilities",
        "description": "Returns the permission keys the member's role grants in the calling organization. Requires the `users:read` scope.",
        "tags": [
          "Members"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's user identifier.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The member's role and the permissions it grants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberCapabilities"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:read` scope."
          },
          "404": {
            "description": "No such member in the calling organization."
          }
        }
      }
    },
    "/v1/org/invitations/{invitationId}": {
      "delete": {
        "operationId": "cancelOrganizationInvitation",
        "summary": "Cancel a pending invitation",
        "description": "Withdraws a pending invitation for the calling organization. An invitation that was already accepted or cancelled, or that belongs to another organization, is reported as not found. Requires the `users:write` scope.",
        "tags": [
          "Members"
        ],
        "parameters": [
          {
            "name": "invitationId",
            "in": "path",
            "required": true,
            "description": "The invitation identifier returned when it was created.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The invitation was cancelled."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:write` scope."
          },
          "404": {
            "description": "No pending invitation with that identifier."
          }
        }
      }
    },
    "/v1/org/users/{userId}/settings": {
      "get": {
        "operationId": "getOrganizationMemberSettings",
        "summary": "Get a member's preferences",
        "description": "Returns one member's interface preferences for the calling organization. Preferences are held separately per organization, so a person who belongs to several carries a different set in each. A member who has saved nothing receives the documented defaults. Requires the `users:read` scope.",
        "tags": [
          "Members"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's user identifier.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The member's preferences.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberSettings"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:read` scope."
          },
          "404": {
            "description": "No such member in the calling organization."
          }
        }
      },
      "put": {
        "operationId": "updateOrganizationMemberSettings",
        "summary": "Update a member's preferences",
        "description": "Updates one member's interface preferences for the calling organization. Only the fields present in the request are changed; the complete resulting preferences are returned. Requires the `users:write` scope.",
        "tags": [
          "Members"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's user identifier.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberSettingsUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preferences after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberSettings"
                }
              }
            }
          },
          "400": {
            "description": "No recognised field was supplied, or a value is outside the accepted set or range."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token does not carry the `users:write` scope."
          },
          "404": {
            "description": "No such member in the calling organization."
          }
        }
      }
    },
    "/v1/org/entitlements": {
      "get": {
        "operationId": "getOrgEntitlements",
        "summary": "Resolve the organization entitlement verdict",
        "description": "Returns the calling organization's derived entitlement verdict for the requested view. The organization is taken from the access token. The salesforce view is platform-only: callable exclusively by Propper-generated Salesforce client applications, not by customer-created ones.",
        "tags": [
          "Entitlements"
        ],
        "parameters": [
          {
            "name": "view",
            "in": "query",
            "required": true,
            "description": "The projection to return. Currently only `salesforce` is supported.",
            "schema": {
              "type": "string",
              "enum": [
                "salesforce"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resolved entitlement verdict. An organization without an active connector still returns 200 (with `gateState: no_connector`); the endpoint reports entitlement and is never itself entitlement-gated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementResolution"
                }
              }
            }
          },
          "400": {
            "description": "The `view` query parameter is missing or unsupported."
          },
          "401": {
            "description": "Missing or invalid access token."
          },
          "403": {
            "description": "The token is not a platform client authorized for the entitlement verdict projection."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    },
    "schemas": {
      "OrganizationProfile": {
        "type": "object",
        "description": "The organization profile.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "The organization display name."
          },
          "dataResidency": {
            "type": "string",
            "enum": [
              "GLOBAL",
              "EU"
            ],
            "description": "The region where the organization's data is stored."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrganizationMember": {
        "type": "object",
        "description": "One organization member.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The membership identifier."
          },
          "role": {
            "type": "string",
            "description": "The member's role key within the organization (for example `org_admin`)."
          },
          "status": {
            "type": "string",
            "description": "The membership status (for example `active`, `pending`, or `inactive`)."
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the member joined the organization."
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "OrganizationMemberList": {
        "type": "object",
        "description": "One page of the membership roster.",
        "properties": {
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationMember"
            }
          },
          "pagination": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "description": "Total members across all pages."
              },
              "page": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              },
              "hasMore": {
                "type": "boolean",
                "description": "Whether another page follows this one."
              }
            }
          }
        }
      },
      "MemberInviteRequest": {
        "type": "object",
        "required": [
          "email",
          "role"
        ],
        "description": "An invitation to create.",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The address to invite. Compared and stored in lower case."
          },
          "role": {
            "type": "string",
            "enum": [
              "viewer",
              "author",
              "publisher",
              "approver",
              "org_admin"
            ],
            "description": "The role the invited member will hold. One of `viewer`, `author`, `publisher`, `approver`, `org_admin`."
          }
        }
      },
      "MemberRoleUpdateRequest": {
        "type": "object",
        "required": [
          "role"
        ],
        "description": "The role to set on a member.",
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "viewer",
              "author",
              "publisher",
              "approver",
              "org_admin"
            ],
            "description": "One of `viewer`, `author`, `publisher`, `approver`, `org_admin`."
          }
        }
      },
      "Invitation": {
        "type": "object",
        "description": "A pending invitation. The invitation link is delivered by email only.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The invitation identifier."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "string",
            "description": "The role the invited member will hold."
          },
          "status": {
            "type": "string",
            "description": "The invitation status, for example `PENDING`."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the invitation stops being redeemable."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MemberCapabilities": {
        "type": "object",
        "description": "The permissions a member's role grants.",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "role": {
            "type": "string",
            "description": "The member's role key."
          },
          "capabilities": {
            "type": "array",
            "description": "Permission keys, sorted. Empty when the role grants none.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "MemberSettings": {
        "type": "object",
        "description": "One member's interface preferences within an organization.",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "timezone": {
            "type": "string",
            "description": "Defaults to `UTC`."
          },
          "language": {
            "type": "string",
            "description": "Defaults to `en`."
          },
          "theme": {
            "type": "string",
            "enum": [
              "light",
              "dark",
              "system"
            ],
            "description": "Defaults to `system`."
          },
          "defaultDashboardView": {
            "type": "string",
            "enum": [
              "overview",
              "acceptances",
              "deployments",
              "analytics"
            ],
            "description": "Defaults to `overview`."
          },
          "itemsPerPage": {
            "type": "integer",
            "minimum": 5,
            "maximum": 200,
            "description": "Rows per page in list views. Defaults to 25."
          },
          "emailNotifications": {
            "$ref": "#/components/schemas/MemberEmailNotifications"
          }
        }
      },
      "MemberSettingsUpdate": {
        "type": "object",
        "description": "The preferences to change. Supply at least one field; omitted fields are left as they are.",
        "properties": {
          "timezone": {
            "type": "string",
            "maxLength": 64
          },
          "language": {
            "type": "string",
            "maxLength": 16
          },
          "theme": {
            "type": "string",
            "enum": [
              "light",
              "dark",
              "system"
            ]
          },
          "defaultDashboardView": {
            "type": "string",
            "enum": [
              "overview",
              "acceptances",
              "deployments",
              "analytics"
            ]
          },
          "itemsPerPage": {
            "type": "integer",
            "minimum": 5,
            "maximum": 200
          },
          "emailNotifications": {
            "$ref": "#/components/schemas/MemberEmailNotifications"
          }
        }
      },
      "MemberEmailNotifications": {
        "type": "object",
        "description": "Which emails the member receives. Any field you omit keeps its default.",
        "properties": {
          "acceptances": {
            "type": "boolean",
            "description": "Defaults to true."
          },
          "deployments": {
            "type": "boolean",
            "description": "Defaults to true."
          },
          "systemUpdates": {
            "type": "boolean",
            "description": "Defaults to true."
          },
          "weeklyReports": {
            "type": "boolean",
            "description": "Defaults to false."
          }
        }
      },
      "EntitlementResolution": {
        "type": "object",
        "description": "The derived entitlement verdict for one organization and view.",
        "properties": {
          "resolvedAt": {
            "type": "string",
            "format": "date-time"
          },
          "org": {
            "type": "object",
            "properties": {
              "propperOrgId": {
                "type": "string"
              },
              "salesforceOrgId": {
                "type": "string",
                "nullable": true
              },
              "isSandbox": {
                "type": "boolean"
              }
            }
          },
          "connector": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean"
              },
              "tier": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "dependency": {
            "type": "object",
            "properties": {
              "satisfied": {
                "type": "boolean"
              },
              "required": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "missing": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "gen": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean"
              }
            }
          },
          "trial": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean"
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "gateState": {
            "type": "string",
            "description": "The verdict state. The service emits `no_sign_gen`, `no_connector`, or `operational`; `no_link` is decided by the integration client and is never emitted by the server.",
            "enum": [
              "no_sign_gen",
              "no_connector",
              "operational"
            ]
          },
          "bonusTxn": {
            "type": "object",
            "nullable": true,
            "properties": {
              "limit": {
                "type": "integer",
                "nullable": true
              },
              "used": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer",
                "nullable": true
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "onboarding": {
            "type": "object",
            "nullable": true,
            "properties": {
              "state": {
                "type": "string"
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "graceTtlSeconds": {
            "type": "integer"
          },
          "links": {
            "type": "object",
            "properties": {
              "manageBilling": {
                "type": "string",
                "nullable": true
              },
              "contactSales": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      }
    }
  }
}