{"openapi": "3.1.0", "info": {"title": "Skilljar API v2", "version": "2.0.0", "description": "The `x-required-scope` extension on each operation lists the OAuth scopes that authorize it. Access is granted when the caller holds any one of the listed scopes (any-of / OR semantics)."}, "paths": {"/v2/auth/token": {"post": {"operationId": "api_v2_oauth_tokens_endpoints_token_exchange", "summary": "Exchange credentials for JWT", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TokenResponse"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}}, "description": "Exchange client credentials or a refresh token for an access token.\n\nSupports three grant types:\n- Client credentials: provide client_id + client_secret.\n- Refresh token: provide grant_type=refresh_token + refresh_token.\n- Authorization code: provide grant_type=authorization_code + code + code_verifier + redirect_uri + client_id.", "tags": ["Auth"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/TokenRequest"}}}, "required": true}}}, "/v2/auth/revoke": {"post": {"operationId": "api_v2_oauth_tokens_endpoints_revoke_token_endpoint", "summary": "Revoke a refresh token", "parameters": [], "responses": {"200": {"description": "OK"}}, "description": "Revoke a refresh token, invalidating it for future use.", "tags": ["Auth"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/RevokeRequest"}}}, "required": true}}}, "/v2/oauth/register": {"post": {"operationId": "register_oauth_client", "summary": "Dynamically register an OAuth2 client (RFC 7591)", "parameters": [], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DynamicClientRegistrationResponse"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}, "405": {"description": "Method Not Allowed", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorSchema"}}}}}, "description": "Register a new OAuth2 client via unauthenticated Dynamic Client Registration.\n\nPublic/PKCE clients (token_endpoint_auth_method == \"none\") receive no\nclient_secret. Confidential clients receive a one-time client_secret that\ncannot be retrieved again after this response.\n\nErrors render as RFC 7591's ``{\"error\", \"error_description\"}`` shape, not\napi_v2's usual JSON:API envelope (mirrors token_exchange) -- see\nclient_service.register_client's docstring for why, and for why no org is\nbound / audited here yet.", "tags": ["Dynamic Client Registration"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DynamicClientRegistrationRequest"}}}, "required": true}}}, "/v2/clients/": {"get": {"operationId": "list_clients", "summary": "List clients", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"additionalProperties": true, "title": "Response", "type": "object"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List OAuth2 clients for the organization.\n\n**Required OAuth scope:** `clients:read`", "tags": ["Clients"], "security": [{"JWTAuth": []}], "x-required-scope": ["clients:read"]}, "post": {"operationId": "create_client", "summary": "Create client", "parameters": [], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"additionalProperties": true, "title": "Response", "type": "object"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create a new OAuth2 client.\n\nReturns 201 with a JSON:API resource object including the one-time\n``client_secret`` in the attributes.  The secret is never retrievable\nafter this response.\n\n**Required OAuth scope:** `clients:write`", "tags": ["Clients"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ClientCreateRequest"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["clients:write"]}}, "/v2/clients/{id}/rotate-secret": {"post": {"operationId": "rotate_client_secret", "summary": "Rotate client secret", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"additionalProperties": true, "title": "Response", "type": "object"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Rotate the secret for an OAuth2 client.\n\n**Required OAuth scope:** `clients:write`", "tags": ["Clients"], "security": [{"JWTAuth": []}], "x-required-scope": ["clients:write"]}}, "/v2/clients/{id}": {"get": {"operationId": "get_client", "summary": "Get client", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"additionalProperties": true, "title": "Response", "type": "object"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single OAuth2 client by ID.\n\n**Required OAuth scope:** `clients:read`", "tags": ["Clients"], "security": [{"JWTAuth": []}], "x-required-scope": ["clients:read"]}, "patch": {"operationId": "update_client", "summary": "Update client", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"additionalProperties": true, "title": "Response", "type": "object"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update an existing OAuth2 client.\n\n**Required OAuth scope:** `clients:write`", "tags": ["Clients"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ClientUpdateRequest"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["clients:write"]}, "delete": {"operationId": "deactivate_client", "summary": "Deactivate client", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Deactivate an OAuth2 client.\n\n**Required OAuth scope:** `clients:write`", "tags": ["Clients"], "security": [{"JWTAuth": []}], "x-required-scope": ["clients:write"]}}, "/v2/scopes/": {"get": {"operationId": "list_scopes", "summary": "List scope catalog + presets", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ScopeCatalogResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Return all API v2 scopes and named preset bundles.\n\nNo database access is performed \u2014 the catalog is derived from\nin-memory constants.  Requires ``clients:read`` scope because anyone\nwho can view API clients needs access to the scope catalog.\n\nReturns a JSON:API response with:\n- ``data``: list of scope resource objects (type, id, attributes)\n- ``meta.presets``: named scope preset bundles\n\n**Required OAuth scope:** `clients:read`", "tags": ["Scopes"], "security": [{"JWTAuth": []}], "x-required-scope": ["clients:read"]}}, "/v2/students/": {"get": {"operationId": "list_students", "summary": "List students", "parameters": [{"in": "query", "name": "filter[email]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact email match (case-insensitive).", "title": "Filter[Email]"}, "required": false, "description": "Exact email match (case-insensitive)."}, {"in": "query", "name": "filter[first_name]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact first name match.", "title": "Filter[First Name]"}, "required": false, "description": "Exact first name match."}, {"in": "query", "name": "filter[last_name]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact last name match.", "title": "Filter[Last Name]"}, "required": false, "description": "Exact last name match."}, {"in": "query", "name": "filter[is_inactive]", "schema": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Filter by active/inactive status.", "title": "Filter[Is Inactive]"}, "required": false, "description": "Filter by active/inactive status."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_StudentResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List students for the authenticated organization.\n\n**Required OAuth scope:** `students:read`", "tags": ["Students"], "security": [{"JWTAuth": []}], "x-required-scope": ["students:read"]}, "post": {"operationId": "create_students", "summary": "Create students", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_StudentResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_StudentResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to BATCH_MAX_ITEMS students in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item is processed independently \u2014 a failure\non one row does not abort the rest. Response is 200 if all items succeed,\n207 if any fail, 400 if the request envelope is empty or over the cap.\n\nWithin-batch duplicate detection is first-wins: the first occurrence of an\nemail is processed; subsequent occurrences are pre-marked as\n``duplicate_in_batch`` without reaching the service.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention\nand per-item error code vocabulary.\n\n**Required OAuth scope:** `students:write`", "tags": ["Students"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/StudentCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["students:write"]}, "patch": {"operationId": "update_students", "summary": "Update students", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_StudentResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_StudentResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to DEFAULT_BATCH_MAX_ITEMS students in a single request.\n\nReplaces single-resource PATCH /v2/students/{id}/ from ACCEL-256 \u2014 never publicly released.\n\nEach item identifies a student via ``id`` (preferred) or\n``attributes.email`` (fallback). The ``email`` field is read-only \u2014\nit serves as the lookup identifier when ``id`` is absent, or as\nconfirmation when ``id`` is present. It cannot be changed via this\nendpoint. Updatable attributes: ``first_name``, ``last_name``,\n``is_inactive``.\n\nEach item is processed independently \u2014 failures on one row do not abort\nthe rest. Response is 200 if all items succeed, 207 if any fail, 400 if\nthe request envelope is empty or over the cap.\n\nPer-item failure ``code`` is one of:\n- ``not_found`` \u2014 student missing in this org\n- ``validation_error`` \u2014 anonymized/SSO/inactive guard rejected the update,\n  or email confirmation did not match the student's current email\n\nTo reactivate AND modify other fields, send two PATCHes:\n\n1. First PATCH: ``{\"data\": [{\"id\": \"...\", \"attributes\": {\"is_inactive\": false}}]}``\n2. Second PATCH: ``{\"data\": [{\"id\": \"...\", \"attributes\": {\"first_name\": \"...\"}}]}``\n\nSee ``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `students:write`", "tags": ["Students"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/StudentUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["students:write"]}}, "/v2/students/{id}": {"get": {"operationId": "get_student", "summary": "Get student", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StudentDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single student by ID.\n\n**Required OAuth scope:** `students:read`", "tags": ["Students"], "security": [{"JWTAuth": []}], "x-required-scope": ["students:read"]}, "delete": {"operationId": "deactivate_student", "summary": "Deactivate student", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Deactivate (soft-delete) a student.\n\n**Required OAuth scope:** `students:deactivate`", "tags": ["Students"], "security": [{"JWTAuth": []}], "x-required-scope": ["students:deactivate"]}}, "/v2/students/{id}/anonymize/": {"post": {"operationId": "anonymize_student", "summary": "Anonymize student", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Permanently erase a student's PII. Requires the X-Confirm-Destructive: true header.\n\n**Required OAuth scope:** `students:anonymize`", "tags": ["Students"], "security": [{"JWTAuth": []}], "x-required-scope": ["students:anonymize"]}}, "/v2/students/{id}/send-password-reset/": {"post": {"operationId": "send_password_reset", "summary": "Send password reset", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}, {"in": "query", "name": "domain", "schema": {"title": "Domain", "type": "string"}, "required": true}], "responses": {"202": {"description": "Accepted"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Send a password reset email to a student. ?domain=learn.example.com is required.\n\n**Required OAuth scope:** `students:manage-password`", "tags": ["Students"], "security": [{"JWTAuth": []}], "x-required-scope": ["students:manage-password"]}}, "/v2/students/{id}/set-password/": {"post": {"operationId": "set_student_password", "summary": "Set student password", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Set a student's password directly. Password must meet the organization's requirements.\n\n**Required OAuth scope:** `students:manage-password`", "tags": ["Students"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SetPasswordRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["students:manage-password"]}}, "/v2/lessons/": {"get": {"operationId": "list_lessons", "summary": "List lessons", "parameters": [{"in": "query", "name": "filter[course_id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent course ID. Exact match.", "title": "Filter[Course Id]"}, "required": false, "description": "Obfuscated parent course ID. Exact match."}, {"in": "query", "name": "filter[type]", "schema": {"anyOf": [{"enum": ["ASSET", "HTML", "QUIZ", "WEB_PACKAGE", "VILT", "IE_EXAM", "WIDGET", "MODULAR"], "type": "string"}, {"type": "null"}], "description": "Lesson type. Exact match. Accepted values: ASSET, HTML, QUIZ, WEB_PACKAGE, VILT, IE_EXAM, WIDGET, MODULAR. Unknown values are rejected with 422.", "title": "Filter[Type]"}, "required": false, "description": "Lesson type. Exact match. Accepted values: ASSET, HTML, QUIZ, WEB_PACKAGE, VILT, IE_EXAM, WIDGET, MODULAR. Unknown values are rejected with 422."}, {"in": "query", "name": "filter[title]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Lesson title. Case-insensitive exact match.", "title": "Filter[Title]"}, "required": false, "description": "Lesson title. Case-insensitive exact match."}, {"in": "query", "name": "filter[updated_since]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns lessons updated at or after this time. Naive datetimes without an offset are rejected with 422.", "title": "Filter[Updated Since]"}, "required": false, "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns lessons updated at or after this time. Naive datetimes without an offset are rejected with 422."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_LessonResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List non-draft lessons for the authenticated organization.\n\n**Required OAuth scope:** `lessons:read`", "tags": ["Lessons"], "security": [{"JWTAuth": []}], "x-required-scope": ["lessons:read"]}, "post": {"operationId": "create_lessons", "summary": "Create lessons", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_LessonDetailResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_LessonDetailResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to BATCH_MAX_ITEMS lessons in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item is processed independently \u2014 a failure\non one row does not abort the rest. Response is 200 if all items succeed,\n207 if any fail, 400 if the request envelope is empty or over the cap.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention\nand per-item error code vocabulary.\n\n**Required OAuth scope:** `lessons:write`", "tags": ["Lessons"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/LessonCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["lessons:write"]}, "patch": {"operationId": "update_lessons", "summary": "Update lessons", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_LessonDetailResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_LessonDetailResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to BATCH_MAX_ITEMS lessons in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item must carry the obfuscated lesson ``id``\nand an ``attributes`` object with the fields to update. A failure on one\nrow does not abort the rest. Response is 200 if all items succeed, 207 if\nany fail, 400 if the request envelope is empty or over the cap.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention\nand per-item error code vocabulary.\n\n**Required OAuth scope:** `lessons:write`", "tags": ["Lessons"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/LessonBatchUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["lessons:write"]}}, "/v2/lessons/{id}": {"get": {"operationId": "get_lesson", "summary": "Get lesson", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LessonDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single lesson by ID.\n\n**Required OAuth scope:** `lessons:read`", "tags": ["Lessons"], "security": [{"JWTAuth": []}], "x-required-scope": ["lessons:read"]}}, "/v2/courses/": {"get": {"operationId": "list_courses", "summary": "List courses", "parameters": [{"in": "query", "name": "filter[title]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Case-insensitive partial match on course title.", "title": "Filter[Title]"}, "required": false, "description": "Case-insensitive partial match on course title."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_CourseResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List non-deleted, non-draft courses for the authenticated organization.\n\n**Required OAuth scope:** `courses:read`", "tags": ["Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["courses:read"]}, "post": {"operationId": "create_courses", "summary": "Create courses", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_CourseResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_CourseResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to ``BATCH_MAX_ITEMS`` courses in a single request.\n\nReturns 200 if all items succeed, 207 if any fail, 400 if the envelope is\nempty or over the cap. Per-row failures (unresolved ``created_by_email``,\nunhandled exceptions) do not abort the batch.\n\nNo within-batch deduplication is applied \u2014 Course has no natural uniqueness\nkey like the student-email constraint that drives the students endpoint's\nfirst-wins dedup pass.\n\n**Required OAuth scope:** `courses:write`", "tags": ["Courses"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CourseCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["courses:write"]}, "patch": {"operationId": "update_courses", "summary": "Update courses", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_CourseResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_CourseResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to ``BATCH_MAX_ITEMS`` courses in a single request.\n\nReturns 200 if all items succeed, 207 if any fail, 400 if the envelope is\nempty or over the cap. Per-row failures (``not_found`` for malformed /\nmissing / cross-org / soft-deleted / draft ids, ``internal_error`` for\nunhandled exceptions) do not abort the batch.\n\n**Required OAuth scope:** `courses:write`", "tags": ["Courses"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CourseUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["courses:write"]}}, "/v2/courses/{id}": {"get": {"operationId": "get_course", "summary": "Get course", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CourseDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single course by ID.\n\n**Required OAuth scope:** `courses:read`", "tags": ["Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["courses:read"]}}, "/v2/analytics/courses/{course_id}": {"get": {"operationId": "get_course_analytics", "summary": "Get course analytics", "parameters": [{"in": "path", "name": "course_id", "schema": {"title": "Course Id", "type": "string"}, "required": true}, {"in": "query", "name": "filter[domains]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; counts only enrollments whose published course is in one of those domains. Unknown names match nothing, never error.", "title": "Filter[Domains]"}, "required": false, "description": "Comma-separated domain names; counts only enrollments whose published course is in one of those domains. Unknown names match nothing, never error."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CourseAnalyticsDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single course's analytics summary, optionally narrowed by ``filter[domains]``.\n\n**Required OAuth scope:** `analytics:read`", "tags": ["Analytics"], "security": [{"JWTAuth": []}], "x-required-scope": ["analytics:read"]}}, "/v2/analytics/courses/{course_id}/ratings/": {"get": {"operationId": "list_course_ratings", "summary": "List a course's ratings", "parameters": [{"in": "path", "name": "course_id", "schema": {"title": "Course Id", "type": "string"}, "required": true}, {"in": "query", "name": "filter[student.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only ratings by the student with this opaque id. Unknown or malformed ids match nothing.", "title": "Filter[Student.Id]"}, "required": false, "description": "Return only ratings by the student with this opaque id. Unknown or malformed ids match nothing."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_CourseRatingResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List a course's student ratings, most recent first, optionally narrowed by ``filter[student.id]``.\n\n**Required OAuth scope:** `analytics:read`", "tags": ["Analytics"], "security": [{"JWTAuth": []}], "x-required-scope": ["analytics:read"]}}, "/v2/certificates/{id}": {"get": {"operationId": "get_certificate", "summary": "Get certificate", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CertificateDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single certificate by ID.\n\n**Required OAuth scope:** `certificates:read`", "tags": ["Certificates"], "security": [{"JWTAuth": []}], "x-required-scope": ["certificates:read"]}}, "/v2/certificates/": {"get": {"operationId": "list_certificates", "summary": "List certificates", "parameters": [{"in": "query", "name": "filter[domains]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; matches any listed domain. Unknown names match nothing.", "title": "Filter[Domains]"}, "required": false, "description": "Comma-separated domain names; matches any listed domain. Unknown names match nothing."}, {"in": "query", "name": "filter[status]", "schema": {"allOf": [{"description": "Expiry-state filter values for GET /v2/certificates.", "enum": ["active", "expired", "all"], "title": "CertificateStatus", "type": "string"}], "default": "all", "description": "Filter by expiry state: active (unexpired), expired, or all. Defaults to all."}, "required": false, "description": "Filter by expiry state: active (unexpired), expired, or all. Defaults to all."}, {"in": "query", "name": "filter[issued_gte]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only certificates issued at or after this ISO 8601 timestamp.", "title": "Filter[Issued Gte]"}, "required": false, "description": "Return only certificates issued at or after this ISO 8601 timestamp."}, {"in": "query", "name": "filter[issued_lte]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only certificates issued at or before this ISO 8601 timestamp.", "title": "Filter[Issued Lte]"}, "required": false, "description": "Return only certificates issued at or before this ISO 8601 timestamp."}, {"in": "query", "name": "filter[student.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only certificates for the student with this opaque id. Unknown ids match nothing.", "title": "Filter[Student.Id]"}, "required": false, "description": "Return only certificates for the student with this opaque id. Unknown ids match nothing."}, {"in": "query", "name": "filter[course.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only certificates for the course with this opaque id. Unknown ids match nothing.", "title": "Filter[Course.Id]"}, "required": false, "description": "Return only certificates for the course with this opaque id. Unknown ids match nothing."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_CertificateResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List the organization's certificates, one row per issued certificate.\n\n**Required OAuth scope:** `certificates:read`", "tags": ["Certificates"], "security": [{"JWTAuth": []}], "x-required-scope": ["certificates:read"]}}, "/v2/groups/": {"get": {"operationId": "list_groups", "summary": "List groups", "parameters": [{"in": "query", "name": "filter[name]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Case-insensitive substring match on group name. Backed by a sequential scan inside the caller's organization; expected typical org cardinality is < 1k groups.", "title": "Filter[Name]"}, "required": false, "description": "Case-insensitive substring match on group name. Backed by a sequential scan inside the caller's organization; expected typical org cardinality is < 1k groups."}, {"in": "query", "name": "filter[category_id]", "schema": {"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}], "description": "Obfuscated id of a StudentGroupCategory. Returns only groups in that category. Undecodable or cross-organization ids return zero results, never an error.", "title": "Filter[Category Id]"}, "required": false, "description": "Obfuscated id of a StudentGroupCategory. Returns only groups in that category. Undecodable or cross-organization ids return zero results, never an error."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_GroupResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List student groups for the authenticated organization.\n\n**Required OAuth scope:** `student-groups:read`", "tags": ["Groups"], "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:read"]}, "post": {"operationId": "create_groups", "summary": "Create groups", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_GroupResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_GroupResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to BATCH_MAX_ITEMS groups in a single request.\n\nEach item is processed independently \u2014 a failure on one row does not abort\nthe rest. Response is 200 if all items succeed, 207 if any fail, 400 if\nthe request envelope is empty or over the cap.\n\nWithin-batch duplicate detection is first-wins on ``name``: the first\noccurrence of a name is processed; subsequent occurrences are pre-marked\nas ``duplicate_in_batch`` without reaching the service.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status\nconvention and per-item error code vocabulary.\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GroupCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}, "patch": {"operationId": "update_groups", "summary": "Update groups", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_GroupResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_GroupResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to BATCH_MAX_ITEMS groups in a single request.\n\nEach item identifies a group via ``id`` (obfuscated, required). Updatable\nattributes: ``name``, ``rule_email_domains``, ``send_course_enrollment_email``,\n``category_id``. At least one mutating field per item is required (enforced\nby the schema).\n\nWithin-batch duplicate detection is first-wins on ``id``: the first\noccurrence of an id is processed; subsequent occurrences are pre-marked\nas ``duplicate_in_batch`` without reaching the service. This matches the\nPOST endpoint's first-wins semantics on ``name``.\n\nA ``null`` value for ``category_id`` is meaningful \u2014 it clears the category\nassignment. Distinguishing \"not provided\" from \"explicitly null\" uses\nPydantic's ``model_fields_set``.\n\nSee ``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GroupUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}, "delete": {"operationId": "delete_groups", "summary": "Delete groups", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Delete up to BATCH_MAX_ITEMS groups in a single request.\n\nHard-deletes the rows (``StudentGroup`` is not a ``SoftDeletionModel``).\nAll FK-back relations cascade \u2014 memberships, visibility overrides, and\nother link tables are removed by the database. Per-row ``.delete()``\nfires ``pre_delete`` and ``post_delete`` naturally, preserving the\nSalesforce sync receiver and the workflow / analytics cleanup hooks.\n\nEach item is processed independently \u2014 a failure on one row does not\nabort the rest. Response is 200 if all items deleted, 207 if any failed,\n400 if the request envelope is empty or over the cap.\n\nWithin-batch duplicate detection is first-wins on ``id``: subsequent\noccurrences are pre-marked ``duplicate_in_batch`` (pointing at\n``/data/{i}/id``) without reaching the service. This matches the POST\nand PATCH endpoints' first-wins semantics.\n\nSee ``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GroupDeleteRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}}, "/v2/groups/{id}": {"get": {"operationId": "get_group", "summary": "Get group", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GroupDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single group by ID.\n\n**Required OAuth scope:** `student-groups:read`", "tags": ["Groups"], "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:read"]}}, "/v2/groups/{id}/relationships/published-course-visibility/": {"get": {"operationId": "list_visibility_overrides", "summary": "List visibility overrides for a group", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}, {"in": "query", "name": "filter[published_course_id]", "schema": {"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}], "description": "Obfuscated id of a PublishedCourse. Returns only overrides targeting that course. Undecodable or cross-organization ids return zero results, never an error.", "title": "Filter[Published Course Id]"}, "required": false, "description": "Obfuscated id of a PublishedCourse. Returns only overrides targeting that course. Undecodable or cross-organization ids return zero results, never an error."}, {"in": "query", "name": "filter[is_visible]", "schema": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "True returns only allowlist overrides (force visible); False returns only blocklist overrides (force hidden); omitting the filter returns both.", "title": "Filter[Is Visible]"}, "required": false, "description": "True returns only allowlist overrides (force visible); False returns only blocklist overrides (force hidden); omitting the filter returns both."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_VisibilityOverrideResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Cursor-paginated list of visibility overrides for the given group.\n\nSupported filters (all optional):\n\n- ``filter[published_course_id]``: obfuscated id. Undecodable or\n  cross-organization ids return an empty page, never an error.\n- ``filter[is_visible]``: ``true`` returns only allowlist rows,\n  ``false`` returns only blocklist rows; omitting the filter returns\n  both.\n\nPagination (``page[size]``, ``page[cursor]``) is read from ``request`` by\n``CursorPaginator`` \u2014 the ``filters`` argument is consumed for the\n``filter[*]`` query params only.\n\nReturns 404 if the group does not exist in the caller's organization\n(document-level \u2014 distinct from per-item ``not_found`` on POST/DELETE,\nwhich surfaces inside a 207 envelope).\n\n**Required OAuth scope:** `student-groups:read`", "tags": ["Groups"], "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:read"]}, "post": {"operationId": "add_visibility_overrides", "summary": "Add visibility overrides for a group", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_VisibilityOverrideResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_VisibilityOverrideResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Idempotently add visibility overrides for the group.\n\nPer JSON:API to-many relationship POST semantics, this is **idempotent**:\nif a row already exists for the\n``(group, published_course, is_visible)`` tuple, it is returned as\n``succeeded`` \u2014 no separate ``already_exists`` error code.\n\nWithin-batch duplicate detection is first-wins on the\n``(published_course_id, is_visible)`` tuple: the first occurrence is\nprocessed; subsequent occurrences are pre-marked ``duplicate_in_batch``\nwith the JSON:API source pointer at ``attributes/published_course_id``.\n\nReturns 404 if the group is not in the caller's organization. The\ngroup lookup runs **before** the envelope guard (empty / over-cap) so\na missing group is always reported as 404 regardless of body content\n\u2014 clients can't infer group existence from the 400-vs-404 boundary.\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/VisibilityOverrideAddRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}, "delete": {"operationId": "remove_visibility_overrides", "summary": "Remove visibility overrides for a group", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Remove visibility overrides for the group.\n\nItems identify rows by ``(published_course_id, is_visible)``. Each item\nis processed in its own ``transaction.atomic()`` block so a per-row\nfailure does not abort the rest (no outer atomic wrapping; each per-row\nblock opens a fresh transaction). Within-batch dedup is first-wins on\nthe same tuple.\n\nReturns 404 if the group is not in the caller's organization.\n\nThe ``id`` field on ``BatchDeletedItem`` echoes the\n``published_course_id`` from the request (not the override's own\nobfuscated id), so clients can correlate inputs to outcomes without\nkeeping a separate lookup table.\n\nThe group lookup runs **before** the envelope guard so a missing group\nis reported as 404 regardless of body content (consistent with the\nGET / POST relationship endpoints).\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/VisibilityOverrideRemoveRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}}, "/v2/groups/{id}/relationships/students/": {"post": {"operationId": "add_group_memberships", "summary": "Add students to a group", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_MembershipResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_MembershipResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Idempotently add students to the group.\n\nPer JSON:API to-many relationship POST semantics, this is **idempotent**:\nif the student is already a member, the existing membership is returned\nas ``succeeded`` \u2014 there is no separate ``already_a_member`` error code.\n\nWithin-batch duplicate detection is first-wins on ``id``: the first\noccurrence is processed; subsequent occurrences are pre-marked\n``duplicate_in_batch`` with the JSON:API source pointer at ``/data/{i}/id``.\n\nReturns 404 if the group is not in the caller's organization. The group\nlookup runs **before** the envelope guard so a missing group is reported\nas 404 regardless of body content \u2014 clients can't infer group existence\nfrom the 400-vs-404 boundary.\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MembershipAddRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}, "delete": {"operationId": "remove_group_memberships", "summary": "Remove students from a group", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Idempotently remove students from the group.\n\nPer JSON:API to-many relationship DELETE semantics, this is\n**idempotent**: if the student is not a member, the row is returned with\n``status: \"deleted\"`` \u2014 there is no separate ``not_a_member`` outcome on\nthe wire (the service-level discriminator tracks the difference for the\naudit log and event dispatch).\n\nWithin-batch duplicate detection is first-wins on ``id``.\n\nReturns 404 if the group is not in the caller's organization.\n\n**Required OAuth scope:** `student-groups:write`", "tags": ["Groups"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MembershipRemoveRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["student-groups:write"]}}, "/v2/enrollments/": {"post": {"operationId": "bulk_enroll_students", "summary": "Bulk-enroll students", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_EnrollmentWriteResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_EnrollmentWriteResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Enroll up to ``BATCH_MAX_ITEMS`` students into one published course.\n\nThe hybrid envelope shares ``published_course_id`` and ``expires_at`` at the\ntop level; ``data`` is the per-row item list. Each row is processed\nindependently \u2014 a failure on one row does not abort the rest. Response is\n200 if every row succeeded, 207 if any row failed, 400 if the request\nenvelope is empty, over the cap, or carries a past ``expires_at`` (matches\nthe ticket AC \"past timestamps return 400, request-level\").\n\nWithin-batch duplicate detection is first-wins: subsequent occurrences of\nan email are pre-marked ``duplicate_in_batch`` without reaching the service.\n\nSee ``api_v2/docs/enrollments.md`` for the per-row failure-code vocabulary,\nreactivation behavior, retry contract, and demo seed prerequisites.\n\n**Required OAuth scope:** `enrollments:write`", "tags": ["Enrollments"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EnrollmentBatchCreateRequest"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["enrollments:write"]}, "patch": {"operationId": "update_enrollments", "summary": "Update enrollments", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_EnrollmentWriteResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_EnrollmentWriteResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Partially update a batch of enrollments, each addressed by its id.\n\n**Required OAuth scope:** `enrollments:write`", "tags": ["Enrollments"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EnrollmentUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["enrollments:write"]}, "get": {"operationId": "list_enrollments", "summary": "List enrollments", "parameters": [{"in": "query", "name": "filter[student.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only enrollments for the student with this opaque id. Unknown ids match nothing.", "title": "Filter[Student.Id]"}, "required": false, "description": "Return only enrollments for the student with this opaque id. Unknown ids match nothing."}, {"in": "query", "name": "filter[student.email]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only enrollments for the student with this email (case-insensitive). Unknown emails match nothing.", "title": "Filter[Student.Email]"}, "required": false, "description": "Return only enrollments for the student with this email (case-insensitive). Unknown emails match nothing."}, {"in": "query", "name": "filter[course.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only enrollments in the course with this opaque id. Unknown or malformed ids match nothing.", "title": "Filter[Course.Id]"}, "required": false, "description": "Return only enrollments in the course with this opaque id. Unknown or malformed ids match nothing."}, {"in": "query", "name": "filter[enrolled_gte]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments created on or after this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Enrolled Gte]"}, "required": false, "description": "Return only enrollments created on or after this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422."}, {"in": "query", "name": "filter[enrolled_lte]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments created on or before this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Enrolled Lte]"}, "required": false, "description": "Return only enrollments created on or before this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422."}, {"in": "query", "name": "filter[completed_gte]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments completed on or after this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Completed Gte]"}, "required": false, "description": "Return only enrollments completed on or after this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422."}, {"in": "query", "name": "filter[completed_lte]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments completed on or before this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Completed Lte]"}, "required": false, "description": "Return only enrollments completed on or before this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422."}, {"in": "query", "name": "filter[progress_status]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated progress states to include: completed, in_progress, not_started.", "title": "Filter[Progress Status]"}, "required": false, "description": "Comma-separated progress states to include: completed, in_progress, not_started."}, {"in": "query", "name": "filter[domains]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; returns only enrollments whose published course is in one of those domains. Unknown names match nothing, never error.", "title": "Filter[Domains]"}, "required": false, "description": "Comma-separated domain names; returns only enrollments whose published course is in one of those domains. Unknown names match nothing, never error."}, {"in": "query", "name": "filter[active]", "schema": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Return only active (true) or inactive (false) enrollments. Active means the enrollment is active and not expired; inactive means deactivated or past its expiry. Omit to return both.", "title": "Filter[Active]"}, "required": false, "description": "Return only active (true) or inactive (false) enrollments. Active means the enrollment is active and not expired; inactive means deactivated or past its expiry. Omit to return both."}, {"in": "query", "name": "include", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated related resources to include in the response. Supported: purchase, student, certificate.", "title": "Include"}, "required": false, "description": "Comma-separated related resources to include in the response. Supported: purchase, student, certificate."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EnrollmentListResponse"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List the organization's enrollments (active and inactive), one row per enrollment.\n\n**Required OAuth scope:** `enrollments:read`", "tags": ["Enrollments"], "security": [{"JWTAuth": []}], "x-required-scope": ["enrollments:read"]}}, "/v2/enrollments/completion": {"patch": {"operationId": "complete_enrollments", "summary": "Bulk-complete enrollments", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_EnrollmentCompletionResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_EnrollmentCompletionResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Set or remove course completion on a batch of enrollments, each addressed by its id.\n\n**Required OAuth scope:** `enrollments:write`", "tags": ["Enrollments"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EnrollmentCompletionRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["enrollments:write"]}}, "/v2/enrollments/{id}": {"get": {"operationId": "get_enrollment", "summary": "Get enrollment", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}, {"in": "query", "name": "include", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Include"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EnrollmentDetailResponse"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single enrollment by ID.\n\n**Required OAuth scope:** `enrollments:read`", "tags": ["Enrollments"], "security": [{"JWTAuth": []}], "x-required-scope": ["enrollments:read"]}}, "/v2/quizzes/": {"get": {"operationId": "list_quizzes", "summary": "List quizzes", "parameters": [{"in": "query", "name": "filter[name]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Quiz name. Case-insensitive exact match.", "title": "Filter[Name]"}, "required": false, "description": "Quiz name. Case-insensitive exact match."}, {"in": "query", "name": "filter[updated_since]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns quizzes updated at or after this time. Naive datetimes without an offset are rejected with 422.", "title": "Filter[Updated Since]"}, "required": false, "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns quizzes updated at or after this time. Naive datetimes without an offset are rejected with 422."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_QuizResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List live quizzes for the authenticated organization.\n\n**Required OAuth scope:** `quizzes:read`", "tags": ["Quizzes"], "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:read"]}, "post": {"operationId": "create_quizzes", "summary": "Create quizzes", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuizResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuizResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to BATCH_MAX_ITEMS quizzes in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item is processed independently \u2014 a failure\non one row does not abort the rest. Response is 200 if all items succeed,\n207 if any fail, 400 if the request envelope is empty or over the cap.\n\nPer-item isolation applies only *after* the envelope is parsed. Any\nschema-level validation error (422) \u2014 a malformed field or a cross-field\ninvariant violation (``check_quiz_cross_field_rules``) on any single item \u2014\nis raised by Pydantic before this function runs and rejects the whole\nrequest. Only failures surfaced by the service inside the loop are isolated\nper item.\n\nUnlike lessons, ``Quiz`` has no parent FK, so there is no ``not_found``\nper-item path on create \u2014 a schema-valid item always persists unless an\nunexpected error occurs.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status\nconvention and per-item error code vocabulary.\n\n**Required OAuth scope:** `quizzes:write`", "tags": ["Quizzes"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuizCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:write"]}, "patch": {"operationId": "update_quizzes", "summary": "Update quizzes", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuizResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuizResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to BATCH_MAX_ITEMS quizzes in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item must carry the obfuscated quiz ``id``\nand an ``attributes`` object with the fields to update. A failure on one row\ndoes not abort the rest. Response is 200 if all items succeed, 207 if any\nfail, 400 if the request envelope is empty or over the cap.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status\nconvention and per-item error code vocabulary.\n\n**Required OAuth scope:** `quizzes:write`", "tags": ["Quizzes"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuizBatchUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:write"]}, "delete": {"operationId": "delete_quizzes", "summary": "Delete quizzes", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Delete up to BATCH_MAX_ITEMS quizzes in a single request (CASCADE soft-delete).\n\nEach item is a resource-identifier object (``{type, id}``). Per item the\nquiz is resolved org-scoped + live (already-deleted / wrong-org / malformed\nids surface as per-item ``not_found``) and, in that item's own atomic block,\nthe quiz's OWN questions and their answers are SOFT-deleted, its\nquestion-bank ASSIGNMENTS are HARD-removed, and finally the quiz itself is\nSOFT-deleted. The shared question banks (and their bank-owned questions /\nanswers) are left untouched \u2014 a quiz only owns questions where\n``question.quiz == quiz``.\n\nWithin-batch duplicate detection is first-wins on ``id``; each deleted item\nechoes the caller's input id. Response is 200 if all deleted, 207 if any\nfailed, 400 if the envelope is empty or over the cap. See\n``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `quizzes:write`", "tags": ["Quizzes"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuizDeleteRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:write"]}}, "/v2/quizzes/{id}": {"get": {"operationId": "get_quiz", "summary": "Get quiz", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuizDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single quiz by ID.\n\n**Required OAuth scope:** `quizzes:read`", "tags": ["Quizzes"], "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:read"]}}, "/v2/quizzes/{quiz_id}/question-banks/": {"get": {"operationId": "list_quiz_question_bank_assignments", "summary": "List quiz question-bank assignments", "parameters": [{"in": "path", "name": "quiz_id", "schema": {"title": "Quiz Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankAssignmentListResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List the question-bank assignments for a quiz.\n\nReturns a plain (unpaginated) ``{\"data\": [...]}`` envelope: a quiz's bank\nassignments are a small bounded set. A malformed / cross-org / soft-deleted\n``quiz_id`` raises a document-level 404 (the quiz is the path resource).\n\n**Required OAuth scope:** `quizzes:read`", "tags": ["Quizzes"], "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:read"]}, "post": {"operationId": "bind_quiz_question_banks", "summary": "Bind question banks to a quiz", "parameters": [{"in": "path", "name": "quiz_id", "schema": {"title": "Quiz Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankAssignmentResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankAssignmentResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Bind up to BATCH_MAX_ITEMS question banks to a quiz in a single request.\n\nThe ``quiz_id`` path resource is resolved once up front \u2014 a malformed /\ncross-org / soft-deleted quiz is a document-level 404. Within-batch duplicate\ndetection is first-wins on ``question_bank_id`` (mirrors ``unbind`` /\n``delete_groups``); subsequent duplicates are pre-marked ``duplicate_in_batch``\nwithout reaching the service, so a logical bind fires its\n``QuestionBankAssignmentBound`` event exactly once. Each surviving item is\nprocessed independently in its own atomic block: a bad ``question_bank_id``\nis a per-item ``not_found``; re-binding an already-bound bank UPDATES the\nrow (``update_or_create`` on the natural key, no IntegrityError). Response is\n200 if all succeed, 207 if any fail, 400 if the envelope is empty or over the\ncap.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention.\n\n**Required OAuth scope:** `quizzes:write`", "tags": ["Quizzes"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankAssignmentBindRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:write"]}, "patch": {"operationId": "update_quiz_question_banks", "summary": "Update quiz question-bank assignments", "parameters": [{"in": "path", "name": "quiz_id", "schema": {"title": "Quiz Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankAssignmentResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankAssignmentResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to BATCH_MAX_ITEMS quiz\u2194question-bank assignments in one request.\n\nThe ``quiz_id`` path resource is resolved once up front (document-level 404).\nEach item carries the ``question_bank_id`` natural key plus the writable\nfields; a bank not assigned to this quiz is a per-item ``not_found``. An empty\nattributes set (only the key) is a no-op success. Within-batch duplicate\ndetection is first-wins on ``question_bank_id`` (mirrors ``bind`` / ``unbind``);\nsubsequent duplicates are pre-marked ``duplicate_in_batch`` without reaching\nthe service, so a logical update fires its ``QuestionBankAssignmentUpdated``\nevent exactly once. Response is 200 if all succeed, 207 if any fail, 400 if the\nenvelope is empty or over the cap.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention.\n\n**Required OAuth scope:** `quizzes:write`", "tags": ["Quizzes"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankAssignmentBatchUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:write"]}, "delete": {"operationId": "unbind_quiz_question_banks", "summary": "Unbind question banks from a quiz", "parameters": [{"in": "path", "name": "quiz_id", "schema": {"title": "Quiz Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Unbind (HARD-delete) up to BATCH_MAX_ITEMS assignments from a quiz.\n\nThe ``quiz_id`` path resource is resolved once up front (document-level 404).\nWithin-batch duplicate detection is first-wins on ``question_bank_id``\n(mirrors ``delete_groups``); subsequent duplicates are pre-marked\n``duplicate_in_batch`` without reaching the service. Each surviving item is\nprocessed in its own atomic block \u2014 a bank not assigned to this quiz is a\nper-item ``not_found``. ``QuestionBankAssignment`` is not a SoftDeletionModel,\nso the rows are permanently removed. Response is 200 if all succeed, 207 if\nany fail, 400 if the envelope is empty or over the cap.\n\nSee ``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `quizzes:write`", "tags": ["Quizzes"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankAssignmentDeleteRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["quizzes:write"]}}, "/v2/question-banks/": {"get": {"operationId": "list_question_banks", "summary": "List question banks", "parameters": [{"in": "query", "name": "filter[name]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Question bank name. Case-insensitive exact match.", "title": "Filter[Name]"}, "required": false, "description": "Question bank name. Case-insensitive exact match."}, {"in": "query", "name": "filter[updated_since]", "schema": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns question banks updated at or after this time. Naive datetimes without an offset are rejected with 422.", "title": "Filter[Updated Since]"}, "required": false, "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns question banks updated at or after this time. Naive datetimes without an offset are rejected with 422."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_QuestionBankResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List live question banks for the authenticated organization.\n\n**Required OAuth scope:** `question-banks:read`", "tags": ["Question Banks"], "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:read"]}, "post": {"operationId": "create_question_banks", "summary": "Create question banks", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to BATCH_MAX_ITEMS question banks in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item is processed independently \u2014 a failure\non one row does not abort the rest. Response is 200 if all items succeed,\n207 if any fail, 400 if the request envelope is empty or over the cap.\n\nLike ``Quiz``, ``QuestionBank`` has no parent FK, so there is no\n``not_found`` per-item path on create \u2014 a schema-valid item always persists\nunless an unexpected error occurs.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status\nconvention and per-item error code vocabulary.\n\n**Required OAuth scope:** `question-banks:write`", "tags": ["Question Banks"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:write"]}, "patch": {"operationId": "update_question_banks", "summary": "Update question banks", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionBankResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to BATCH_MAX_ITEMS question banks in a single request.\n\nThe ``data`` array must contain at least one item and at most\n``BATCH_MAX_ITEMS`` items. Each item must carry the obfuscated question-bank\n``id`` and an ``attributes`` object with the fields to update. A failure on\none row does not abort the rest. Response is 200 if all items succeed, 207 if\nany fail, 400 if the request envelope is empty or over the cap.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status\nconvention and per-item error code vocabulary.\n\n**Required OAuth scope:** `question-banks:write`", "tags": ["Question Banks"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankBatchUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:write"]}, "delete": {"operationId": "delete_question_banks", "summary": "Delete question banks", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Delete up to BATCH_MAX_ITEMS question banks in a single request (CASCADE soft-delete).\n\nEach item is a resource-identifier object (``{type, id}``). Per item the\nbank is resolved org-scoped + live (already-deleted / wrong-org / malformed\nids surface as per-item ``not_found``) and, in that item's own atomic block,\nthe bank's questions and their answers are SOFT-deleted, every\n``QuestionBankAssignment`` referencing the bank is HARD-removed (unbinding\nany quizzes that used it), and finally the bank itself is SOFT-deleted. The\nquizzes that referenced the bank are left ALIVE \u2014 only their assignment rows\nare removed.\n\nWithin-batch duplicate detection is first-wins on ``id``; each deleted item\nechoes the caller's input id. Response is 200 if all deleted, 207 if any\nfailed, 400 if the envelope is empty or over the cap. See\n``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `question-banks:write`", "tags": ["Question Banks"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankDeleteRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:write"]}}, "/v2/question-banks/{id}": {"get": {"operationId": "get_question_bank", "summary": "Get question bank", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBankDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single question bank by ID.\n\n**Required OAuth scope:** `question-banks:read`", "tags": ["Question Banks"], "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:read"]}}, "/v2/questions/": {"get": {"operationId": "list_questions", "summary": "List questions", "parameters": [{"in": "query", "name": "filter[quiz_id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent quiz ID. Exact match.", "title": "Filter[Quiz Id]"}, "required": false, "description": "Obfuscated parent quiz ID. Exact match."}, {"in": "query", "name": "filter[question_bank_id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent question bank ID. Exact match.", "title": "Filter[Question Bank Id]"}, "required": false, "description": "Obfuscated parent question bank ID. Exact match."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_QuestionResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List live questions for the authenticated organization.\n\n**Required OAuth scope:** `question-banks:read` or `quizzes:read`", "tags": ["Questions"], "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:read", "quizzes:read"]}, "post": {"operationId": "create_questions", "summary": "Create questions", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to BATCH_MAX_ITEMS questions (each with its answers) in one request.\n\nA question is homed under a quiz XOR a question bank, so the write route\naccepts either ``quizzes:write`` OR ``question-banks:write`` (OR logic). The\n``data`` array must contain at least one item and at most ``BATCH_MAX_ITEMS``\nitems. Each item is processed independently in its own atomic block \u2014 a\nfailure on one row does not abort the rest. Response is 200 if all items\nsucceed, 207 if any fail, 400 if the request envelope is empty or over the\ncap.\n\nA malformed / missing / cross-org / soft-deleted parent surfaces as a\nper-item ``not_found`` (like ``create_lessons``' bad-course path). Per-type,\nXOR, and shape validation happen up-front in Pydantic and return a\ndocument-level 422.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention\nand per-item error code vocabulary.\n\n**Required OAuth scope:** `question-banks:write` or `quizzes:write`", "tags": ["Questions"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:write", "quizzes:write"]}, "patch": {"operationId": "update_questions", "summary": "Update questions", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_QuestionResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to BATCH_MAX_ITEMS questions (QUESTION FIELDS ONLY) in one request.\n\nAnswers are IMMUTABLE on update, so this edits the question's own fields only.\nA question is homed under a quiz XOR a question bank, so the write route accepts\neither ``quizzes:write`` OR ``question-banks:write`` (OR logic). The ``data``\narray must contain at least one item and at most ``BATCH_MAX_ITEMS`` items. Each\nitem carries the obfuscated question ``id`` and an ``attributes`` object with the\nfields to update, and is processed independently in its own atomic block \u2014 a\nfailure on one row does not abort the rest. Response is 200 if all items succeed,\n207 if any fail, 400 if the request envelope is empty or over the cap.\n\nRead-only fields (question_type, quiz_id / question_bank_id, answers, order) are\nrejected with a document-level 422 via ``extra=\"forbid\"``. A cross-state FLAG\nconflict against the stored ``question_type`` (e.g. case_sensitive on a stored\nMULTIPLE_CHOICE) surfaces as a per-item ``validation_error`` (207), not a 422.\n\nDuplicate ids within one batch are NOT de-duplicated: each item is applied in\norder, so repeated ids are last-write-wins (the final item for that id wins, and\neach occurrence emits its own update event). This matches ``batch_update_students``;\nper ``api_v2/docs/batch-responses.md`` within-batch dedup is the endpoint's choice\nand update endpoints accept last-write-wins rather than pre-marking duplicates.\n\nSee ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention and\nper-item error code vocabulary.\n\n**Required OAuth scope:** `question-banks:write` or `quizzes:write`", "tags": ["Questions"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionBatchUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:write", "quizzes:write"]}, "delete": {"operationId": "delete_questions", "summary": "Delete questions", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchDeleteResultEnvelope"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Delete up to BATCH_MAX_ITEMS questions in a single request (CASCADE soft-delete).\n\nA question is homed under a quiz XOR a question bank, so the write route\naccepts either ``quizzes:write`` OR ``question-banks:write`` (OR logic). Each\nitem is a resource-identifier object (``{type, id}``). Per item the question\nis resolved org-scoped + live (already-deleted / wrong-org / malformed ids\nsurface as per-item ``not_found``) and, in that item's own atomic block, the\nquestion's answers are SOFT-deleted and then the question itself is\nSOFT-deleted. The parent quiz / question bank (and its other questions) are\nleft untouched.\n\nWithin-batch duplicate detection is first-wins on ``id``; each deleted item\nechoes the caller's input id. Response is 200 if all deleted, 207 if any\nfailed, 400 if the envelope is empty or over the cap. See\n``api_v2/docs/batch-responses.md`` for the envelope shape.\n\n**Required OAuth scope:** `question-banks:write` or `quizzes:write`", "tags": ["Questions"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionDeleteRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:write", "quizzes:write"]}}, "/v2/questions/{id}": {"get": {"operationId": "get_question", "summary": "Get question", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single question by ID, with its answers nested inline.\n\n**Required OAuth scope:** `question-banks:read` or `quizzes:read`", "tags": ["Questions"], "security": [{"JWTAuth": []}], "x-required-scope": ["question-banks:read", "quizzes:read"]}}, "/v2/published-courses/": {"get": {"operationId": "list_published_courses", "summary": "List published courses", "parameters": [{"in": "query", "name": "filter[domain]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque domain id. Returns only published courses on that domain. Unknown ids match nothing.", "title": "Filter[Domain]"}, "required": false, "description": "Opaque domain id. Returns only published courses on that domain. Unknown ids match nothing."}, {"in": "query", "name": "filter[course]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque course id. Returns only published courses for that course. Unknown ids match nothing.", "title": "Filter[Course]"}, "required": false, "description": "Opaque course id. Returns only published courses for that course. Unknown ids match nothing."}, {"in": "query", "name": "filter[live]", "schema": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Filter by published (live) state. Omit to return both live and unpublished rows.", "title": "Filter[Live]"}, "required": false, "description": "Filter by published (live) state. Omit to return both live and unpublished rows."}, {"in": "query", "name": "include", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated related resources to include. Supported: course, domain.", "title": "Include"}, "required": false, "description": "Comma-separated related resources to include. Supported: course, domain."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_PublishedCourseResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List the organization's published courses (live and unpublished by default).\n\n**Required OAuth scope:** `published-courses:read`", "tags": ["Published Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:read"]}, "post": {"operationId": "publish_courses", "summary": "Publish courses", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_PublishedCourseResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_PublishedCourseResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Publish up to ``BATCH_MAX_ITEMS`` courses to domains in a single request.\n\nReturns 200 if all items succeed, 207 if any fail, 400 if the envelope is empty\nor over the cap. Publishing the same course to the same domain twice is a\nper-item conflict (``already_published``), not a whole-batch failure.\n\n**Required OAuth scope:** `published-courses:write`", "tags": ["Published Courses"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishedCourseCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:write"]}, "patch": {"operationId": "update_published_courses", "summary": "Update published courses", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_PublishedCourseResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_PublishedCourseResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to ``BATCH_MAX_ITEMS`` published courses in a single request.\n\n``course`` / ``domain`` cannot change after publish and are not accepted here.\nReturns 200 if all items succeed, 207 if any fail, 400 if empty or over the cap.\n\n**Required OAuth scope:** `published-courses:write`", "tags": ["Published Courses"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishedCourseUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:write"]}}, "/v2/published-courses/{id}/unpublish/": {"post": {"operationId": "unpublish_published_course", "summary": "Unpublish a course", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishedCourseDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Unpublish a course from its domain: sets live=false and frees the slug.\n\n**Required OAuth scope:** `published-courses:write`", "tags": ["Published Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:write"]}}, "/v2/published-courses/{id}/publish/": {"post": {"operationId": "republish_published_course", "summary": "Re-publish a course", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishedCourseDetailResponse"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Re-publish a previously unpublished course: sets live=true and reassigns the slug.\n\n**Required OAuth scope:** `published-courses:write`", "tags": ["Published Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:write"]}}, "/v2/published-courses/{id}": {"get": {"operationId": "get_published_course", "summary": "Get published course", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishedCourseDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single published course by id.\n\n**Required OAuth scope:** `published-courses:read`", "tags": ["Published Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:read"]}, "delete": {"operationId": "delete_published_course", "summary": "Delete (unpublish) a published course", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Delete a published course \u2014 a soft unpublish, matching v1 DELETE behaviour.\n\n**Required OAuth scope:** `published-courses:write`", "tags": ["Published Courses"], "security": [{"JWTAuth": []}], "x-required-scope": ["published-courses:write"]}}, "/v2/domains/": {"get": {"operationId": "list_domains", "summary": "List domains", "parameters": [{"in": "query", "name": "filter[access]", "schema": {"anyOf": [{"enum": ["PUBLIC", "PRIVATE", "PRIVATE_CODE"], "type": "string"}, {"type": "null"}], "description": "Return only domains with this access mode: PUBLIC, PRIVATE, or PRIVATE_CODE.", "title": "Filter[Access]"}, "required": false, "description": "Return only domains with this access mode: PUBLIC, PRIVATE, or PRIVATE_CODE."}, {"in": "query", "name": "filter[name]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact hostname match. Returns the single domain with that name, if any.", "title": "Filter[Name]"}, "required": false, "description": "Exact hostname match. Returns the single domain with that name, if any."}, {"in": "query", "name": "include", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated related resources to include. Supported: theme.", "title": "Include"}, "required": false, "description": "Comma-separated related resources to include. Supported: theme."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_DomainResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List the organization's domains.\n\n**Required OAuth scope:** `domains:read`", "tags": ["Domains"], "security": [{"JWTAuth": []}], "x-required-scope": ["domains:read"]}}, "/v2/domains/{id}": {"get": {"operationId": "get_domain", "summary": "Get domain", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DomainDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single domain by id.\n\n**Required OAuth scope:** `domains:read`", "tags": ["Domains"], "security": [{"JWTAuth": []}], "x-required-scope": ["domains:read"]}}, "/v2/signup-field-values/": {"post": {"operationId": "create_signup_field_values", "summary": "Create signup field values", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_SignupFieldValueResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_SignupFieldValueResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Bulk-create or update signup field values for a student.\n\nEach item in ``data`` identifies a signup field by ``id`` (obfuscated\n``SignupField`` pk) and sets its value for the given student. Existing\nvalues are overwritten (upsert). Response is 200 if all items succeed,\n207 if any fail, 400 if the batch is empty, 404 if the student is unknown\nor has no memberships in the organization.\n\n**Required OAuth scope:** `signup-fields:write`", "tags": ["Signup Field Values"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SignupFieldValueCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["signup-fields:write"]}, "patch": {"operationId": "update_signup_field_values", "summary": "Update signup field values", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_SignupFieldValueResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_SignupFieldValueResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Partially update a batch of captured signup field values, each addressed by its id.\n\n**Required OAuth scope:** `signup-fields:write`", "tags": ["Signup Field Values"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SignupFieldValueUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["signup-fields:write"]}, "get": {"operationId": "list_signup_field_values", "summary": "List signup field values", "parameters": [{"in": "query", "name": "filter[student.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only values for the student with this opaque id. Unknown ids match nothing.", "title": "Filter[Student.Id]"}, "required": false, "description": "Return only values for the student with this opaque id. Unknown ids match nothing."}, {"in": "query", "name": "filter[signup-field.id]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only values for the signup field with this opaque id. Unknown ids match nothing.", "title": "Filter[Signup-Field.Id]"}, "required": false, "description": "Return only values for the signup field with this opaque id. Unknown ids match nothing."}, {"in": "query", "name": "filter[domains]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; returns only values whose signup field is in one of those domains. Unknown names match nothing, never error.", "title": "Filter[Domains]"}, "required": false, "description": "Comma-separated domain names; returns only values whose signup field is in one of those domains. Unknown names match nothing, never error."}, {"in": "query", "name": "page[size]", "schema": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "required": false, "description": "Maximum number of records to return."}, {"in": "query", "name": "page[cursor]", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}, "required": false, "description": "Opaque cursor from the previous page's links.next."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_SignupFieldValueResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List the organization's captured signup-field values, one row per value.\n\n**Required OAuth scope:** `signup-fields:read`", "tags": ["Signup Field Values"], "security": [{"JWTAuth": []}], "x-required-scope": ["signup-fields:read"]}}, "/v2/signup-field-values/{signup_field_value_id}": {"get": {"operationId": "get_signup_field_value", "summary": "Get signup field value", "parameters": [{"in": "path", "name": "signup_field_value_id", "schema": {"title": "Signup Field Value Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SignupFieldValueResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single signup field value by ID.\n\n**Required OAuth scope:** `signup-fields:read`", "tags": ["Signup Field Values"], "security": [{"JWTAuth": []}], "x-required-scope": ["signup-fields:read"]}}, "/v2/web-packages/": {"get": {"operationId": "list_web_packages", "summary": "List web packages", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedListResponse_WebPackageResource_"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "List live web packages for the authenticated organization.\n\n**Required OAuth scope:** `web-packages:read`", "tags": ["Web Packages"], "security": [{"JWTAuth": []}], "x-required-scope": ["web-packages:read"]}, "post": {"operationId": "create_web_packages", "summary": "Create web packages", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_WebPackageResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_WebPackageResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Create up to ``BATCH_MAX_ITEMS`` web packages in a single request.\n\nEach item supplies a ``content_url`` that the server fetches and re-hosts\n**asynchronously**. The response therefore describes rows in state\n``PROCESSING`` \u2014 poll ``GET /v2/web-packages/{id}`` until ``state`` becomes\n``READY`` or ``ERROR``. A malformed archive surfaces as ``state=ERROR``, not\nas an error on this response.\n\nReturns 200 if all items succeed, 207 if any fail, 400 if the envelope is\nempty or over the cap.\n\nRate limited more tightly than a plain row-writing batch endpoint: every\naccepted item queues an outbound fetch of up to ``REMOTE_FETCH_MAX_BYTES``,\nso this endpoint converts one request into real egress. Actual concurrent\nfetching is additionally bounded by Celery worker concurrency.\n\nNo within-batch deduplication is applied \u2014 ``WebPackage`` has no natural\nuniqueness key, and two identical ``content_url`` values are a legitimate\nrequest for two distinct packages.\n\n**Required OAuth scope:** `web-packages:write`", "tags": ["Web Packages"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebPackageCreateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["web-packages:write"]}, "patch": {"operationId": "update_web_packages", "summary": "Update web packages", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_WebPackageResource_"}}}}, "207": {"description": "Multi-Status", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BatchResultEnvelope_WebPackageResource_"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Update up to ``BATCH_MAX_ITEMS`` web packages in a single request.\n\n``title`` is the only writable attribute. ``type``, ``state`` and\n``base_path`` are server-owned, and ``display_name`` is derived from the\ntitle, state and archive filename \u2014 a client that sends any of them has them\nsilently ignored rather than rejected.\n\nNote ``display_name`` only reflects ``title`` once the package reaches\n``READY``. While a package is ``PROCESSING`` or ``ERROR`` its display name is\nthe state and filename, so a title change appears to have no effect on it\nuntil ingestion finishes. That is ``WebPackage.build_display_name``'s\nbehaviour, unchanged by this API.\n\nReturns 200 if all items succeed, 207 if any fail, 400 if the envelope is\nempty or over the cap.\n\n**Required OAuth scope:** `web-packages:write`", "tags": ["Web Packages"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebPackageUpdateRequestEnvelope"}}}, "required": true}, "security": [{"JWTAuth": []}], "x-required-scope": ["web-packages:write"]}}, "/v2/web-packages/{id}": {"get": {"operationId": "get_web_package", "summary": "Get web package", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebPackageDetailResponse"}}}}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Retrieve a single web package by ID.\n\n**Required OAuth scope:** `web-packages:read`", "tags": ["Web Packages"], "security": [{"JWTAuth": []}], "x-required-scope": ["web-packages:read"]}, "delete": {"operationId": "delete_web_package", "summary": "Delete web package", "parameters": [{"in": "path", "name": "id", "schema": {"title": "Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}, "422": {"description": "Unprocessable Entity", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}}}}}, "description": "Soft-delete a web package.\n\nReturns 409 when a lesson in a live course still references the package \u2014\ndeleting it would leave that lesson with no content. Otherwise the package\nis soft-deleted and 204 is returned.\n\nBy-id rather than a batch envelope: the conflict outcome has no natural home\nin a per-row batch result, and ``batch-responses.md`` warns specifically\nagainst letting a ``ConflictError`` fall through the document-level path\nmid-batch. Matches ``DELETE /v2/students/{id}``.\n\n**Required OAuth scope:** `web-packages:write`", "tags": ["Web Packages"], "security": [{"JWTAuth": []}], "x-required-scope": ["web-packages:write"]}}}, "components": {"schemas": {"TokenResponse": {"description": "Successful token issuance response.", "properties": {"access_token": {"description": "Short-lived JWT for authenticating API requests. Pass as Bearer token in Authorization header.", "title": "Access Token", "type": "string"}, "refresh_token": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Long-lived token for obtaining new access tokens. Store securely.", "title": "Refresh Token"}, "expires_in": {"description": "Seconds until the access_token expires.", "title": "Expires In", "type": "integer"}, "token_type": {"default": "Bearer", "description": "Always \"Bearer\".", "title": "Token Type", "type": "string"}}, "required": ["access_token", "expires_in"], "title": "TokenResponse", "type": "object"}, "ErrorSchema": {"description": "RFC 6749 \u00a75.2 error response.", "properties": {"error": {"description": "RFC 6749 error code, e.g. invalid_client, invalid_grant.", "title": "Error", "type": "string"}, "error_description": {"default": "", "description": "Human-readable explanation of the error.", "title": "Error Description", "type": "string"}}, "required": ["error"], "title": "ErrorSchema", "type": "object"}, "TokenRequest": {"description": "Request body for obtaining an access token.\n\nSupports three grant types:\n- ``client_credentials``: provide client_id + client_secret.\n- ``refresh_token``: provide refresh_token.\n- ``authorization_code``: provide code + code_verifier + redirect_uri + client_id (PKCE S256).", "properties": {"grant_type": {"default": "client_credentials", "description": "Grant type. Use \"client_credentials\" for service accounts, \"refresh_token\" to rotate tokens, or \"authorization_code\" for PKCE auth-code exchange.", "enum": ["client_credentials", "refresh_token", "authorization_code"], "title": "Grant Type", "type": "string"}, "client_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "OAuth2 client ID. Required for client_credentials and authorization_code grants.", "title": "Client Id"}, "client_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "OAuth2 client secret. Required for client_credentials grant.", "title": "Client Secret"}, "refresh_token": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Refresh token issued by a prior token exchange. Required for refresh_token grant.", "title": "Refresh Token"}, "code": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Authorization code. Required for authorization_code grant.", "title": "Code"}, "code_verifier": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "PKCE code verifier. Required for authorization_code grant.", "title": "Code Verifier"}, "redirect_uri": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Redirect URI matching the one used at authorization. Required for authorization_code grant.", "title": "Redirect Uri"}}, "title": "TokenRequest", "type": "object"}, "RevokeRequest": {"description": "Request body for revoking a token.", "properties": {"token": {"description": "The refresh token to revoke.", "minLength": 1, "title": "Token", "type": "string"}, "token_type_hint": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional hint. Use \"refresh_token\".", "title": "Token Type Hint"}}, "required": ["token"], "title": "RevokeRequest", "type": "object"}, "DynamicClientRegistrationResponse": {"description": "RFC 7591 dynamic client registration response.\n\n``client_secret`` is only present for confidential clients\n(``token_endpoint_auth_method != \"none\"``); omitted (not null) for\npublic/PKCE clients so callers can distinguish \"no secret issued\" from\n\"field not applicable.\"", "properties": {"client_id": {"title": "Client Id", "type": "string"}, "client_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Client Secret"}, "client_name": {"title": "Client Name", "type": "string"}, "redirect_uris": {"items": {"type": "string"}, "title": "Redirect Uris", "type": "array"}, "grant_types": {"items": {"type": "string"}, "title": "Grant Types", "type": "array"}, "token_endpoint_auth_method": {"title": "Token Endpoint Auth Method", "type": "string"}, "scope": {"title": "Scope", "type": "string"}}, "required": ["client_id", "client_name", "redirect_uris", "grant_types", "token_endpoint_auth_method", "scope"], "title": "DynamicClientRegistrationResponse", "type": "object"}, "DynamicClientRegistrationRequest": {"description": "RFC 7591 dynamic client registration request body.\n\n``redirect_uris`` is validated in the service layer, not here, so an\ninvalid URI surfaces as the domain-specific 400 rather than a generic\nPydantic 422 (see .claude/rules/api-v2.md).", "properties": {"client_name": {"maxLength": 255, "title": "Client Name", "type": "string"}, "redirect_uris": {"items": {"type": "string"}, "title": "Redirect Uris", "type": "array"}, "grant_types": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "title": "Grant Types"}, "token_endpoint_auth_method": {"default": "client_secret_post", "title": "Token Endpoint Auth Method", "type": "string"}, "scope": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Scope"}, "resource": {"default": "", "title": "Resource", "type": "string"}}, "required": ["client_name"], "title": "DynamicClientRegistrationRequest", "type": "object"}, "ErrorEnvelope": {"description": "JSON:API error document.\n\nSee: https://jsonapi.org/format/#errors", "properties": {"errors": {"items": {"$ref": "#/components/schemas/ErrorObject"}, "title": "Errors", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Optional metadata, e.g. {\"request_id\": \"...\"}.", "title": "Meta"}}, "required": ["errors"], "title": "ErrorEnvelope", "type": "object"}, "ErrorObject": {"description": "JSON:API error object.\n\nSee: https://jsonapi.org/format/#error-objects", "properties": {"status": {"description": "HTTP status code as a string.", "title": "Status", "type": "string"}, "code": {"description": "Application-specific error code.", "title": "Code", "type": "string"}, "title": {"description": "Short, human-readable summary of the error.", "title": "Title", "type": "string"}, "detail": {"description": "Full explanation of the error.", "title": "Detail", "type": "string"}, "source": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pointer to the offending field, e.g. {\"pointer\": \"/data/attributes/email\"}.", "title": "Source"}}, "required": ["status", "code", "title", "detail"], "title": "ErrorObject", "type": "object"}, "ClientCreateRequest": {"description": "Request body for creating a new API client.", "properties": {"name": {"maxLength": 255, "title": "Name", "type": "string"}, "description": {"anyOf": [{"maxLength": 500, "type": "string"}, {"type": "null"}], "title": "Description"}, "scope_codenames": {"items": {"type": "string"}, "title": "Scope Codenames", "type": "array"}, "scope_preset": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Scope Preset"}, "ip_allowlist": {"items": {"type": "string"}, "maxItems": 100, "title": "Ip Allowlist", "type": "array"}}, "required": ["name"], "title": "ClientCreateRequest", "type": "object"}, "ClientUpdateRequest": {"description": "Request body for updating an existing API client.", "properties": {"name": {"anyOf": [{"maxLength": 255, "type": "string"}, {"type": "null"}], "title": "Name"}, "description": {"anyOf": [{"maxLength": 500, "type": "string"}, {"type": "null"}], "title": "Description"}, "scope_codenames": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "title": "Scope Codenames"}, "scope_preset": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Scope Preset"}, "ip_allowlist": {"anyOf": [{"items": {"type": "string"}, "maxItems": 100, "type": "array"}, {"type": "null"}], "title": "Ip Allowlist"}}, "title": "ClientUpdateRequest", "type": "object"}, "ScopeAttributes": {"description": "Attributes for a single scope resource.\n\nAttributes:\n    codename: The scope identifier (e.g. ``\"courses:read\"``).\n    description: Human-readable description of the scope.\n    category: Logical grouping for the scope (e.g. ``\"courses\"``).", "properties": {"codename": {"title": "Codename", "type": "string"}, "description": {"title": "Description", "type": "string"}, "category": {"title": "Category", "type": "string"}}, "required": ["codename", "description", "category"], "title": "ScopeAttributes", "type": "object"}, "ScopeCatalogMeta": {"description": "Meta block for the scope catalog response.\n\nAttributes:\n    presets: Mapping of preset name to list of scope codenames.", "properties": {"presets": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "title": "Presets", "type": "object"}}, "required": ["presets"], "title": "ScopeCatalogMeta", "type": "object"}, "ScopeCatalogResponse": {"description": "Response for GET /v2/scopes/.\n\nReturns the full list of API v2 scopes as JSON:API resource objects,\nplus named preset bundles in the ``meta`` block.\n\nAttributes:\n    data: List of scope resource objects.\n    meta: Scope preset bundles.", "properties": {"data": {"items": {"$ref": "#/components/schemas/ScopeResource"}, "title": "Data", "type": "array"}, "meta": {"$ref": "#/components/schemas/ScopeCatalogMeta"}}, "required": ["data", "meta"], "title": "ScopeCatalogResponse", "type": "object"}, "ScopeResource": {"description": "JSON:API resource object for a scope.\n\n``id`` is set to the scope codename for easy client-side keying.\n\nAttributes:\n    type: Always ``\"scopes\"``.\n    id: The scope codename (same as ``attributes.codename``).\n    attributes: The scope attributes.", "properties": {"type": {"title": "Type", "type": "string"}, "id": {"title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/ScopeAttributes"}}, "required": ["type", "id", "attributes"], "title": "ScopeResource", "type": "object"}, "StudentFilters": {"description": "Query parameters for GET /v2/students.\n\nRedeclares the four filter fields with ``filter[*]`` aliases so the HTTP\nsurface only accepts the JSON:API bracketed form (a bare ``?email=...``\nquery param is silently ignored as an extra key). Adds ``limit`` / ``cursor``\nfor ``CursorPaginator``.", "properties": {"filter[email]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact email match (case-insensitive).", "title": "Filter[Email]"}, "filter[first_name]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact first name match.", "title": "Filter[First Name]"}, "filter[last_name]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact last name match.", "title": "Filter[Last Name]"}, "filter[is_inactive]": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Filter by active/inactive status.", "title": "Filter[Is Inactive]"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "StudentFilters", "type": "object"}, "PaginatedListResponse_StudentResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/StudentResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[StudentResource]", "type": "object"}, "StudentAttributes": {"description": "Attributes of a student resource object.", "properties": {"email": {"description": "Student's email address.", "title": "Email", "type": "string"}, "first_name": {"description": "Student's given name.", "title": "First Name", "type": "string"}, "last_name": {"description": "Student's family name.", "title": "Last Name", "type": "string"}, "is_inactive": {"description": "True if the student has been deactivated.", "title": "Is Inactive", "type": "boolean"}, "external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "date_joined": {"description": "Timestamp when the student joined.", "format": "date-time", "title": "Date Joined", "type": "string"}}, "required": ["email", "first_name", "last_name", "is_inactive", "external_id", "date_joined"], "title": "StudentAttributes", "type": "object"}, "StudentResource": {"description": "JSON:API resource object for a student.", "properties": {"type": {"default": "students", "description": "Always \"students\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque student ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/StudentAttributes"}, "relationships": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Related resource links.", "title": "Relationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "StudentResource", "type": "object"}, "BatchFailedItem": {"description": "Wrapper for a failed item in a 207 batch response.", "properties": {"status": {"const": "failed", "default": "failed", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Echoed resource ID if the input identified a row.", "title": "Id"}, "error": {"$ref": "#/components/schemas/BatchItemError", "description": "Error details for the failed item."}}, "required": ["error"], "title": "BatchFailedItem", "type": "object"}, "BatchItemError": {"description": "Per-item error inside a 207 batch response.\n\nAligned with JSON:API ``ErrorObject`` field naming (``code`` rather than\n``reason``) so consumers can reuse error-handling logic across document-level\nerrors (``ErrorObject`` in ``ErrorEnvelope``) and per-item errors (here).\nDifferences from ``ErrorObject``: ``status``/``title`` are omitted because\nthey're redundant for the 207-batch context (HTTP status is on the envelope,\nand the title is derivable from ``code``).", "properties": {"code": {"description": "Machine-readable error code.", "enum": ["duplicate_email", "duplicate_in_batch", "duplicate_name", "validation_error", "not_found", "internal_error", "not_in_domain", "already_enrolled", "already_published"], "title": "Code", "type": "string"}, "source": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pointer to the offending input slot, e.g. {\"pointer\": \"/data/0/attributes/email\"}.", "title": "Source"}, "detail": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Human-readable explanation of the error.", "title": "Detail"}}, "required": ["code"], "title": "BatchItemError", "type": "object"}, "BatchResultEnvelope_StudentResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_StudentResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[StudentResource]", "type": "object"}, "BatchSucceededItem_StudentResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/StudentResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[StudentResource]", "type": "object"}, "BatchSummary": {"description": "Aggregate counts for a 207 batch response.\n\nInvariant: ``succeeded + failed == total``. Enforced by ``@model_validator``.", "properties": {"total": {"description": "Total number of items submitted.", "minimum": 0, "title": "Total", "type": "integer"}, "succeeded": {"description": "Number of items that succeeded.", "minimum": 0, "title": "Succeeded", "type": "integer"}, "failed": {"description": "Number of items that failed.", "minimum": 0, "title": "Failed", "type": "integer"}}, "required": ["total", "succeeded", "failed"], "title": "BatchSummary", "type": "object"}, "StudentCreateRequest": {"description": "Request schema for creating a new student.", "properties": {"email": {"description": "Student's email address. Normalized to lowercase on save.", "format": "email", "title": "Email", "type": "string"}, "first_name": {"default": "", "description": "Student's given name. Maximum 50 characters.", "maxLength": 50, "title": "First Name", "type": "string"}, "last_name": {"default": "", "description": "Student's family name. Maximum 50 characters.", "maxLength": 50, "title": "Last Name", "type": "string"}}, "required": ["email"], "title": "StudentCreateRequest", "type": "object"}, "StudentCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/students/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<=200 items) lives in the endpoint, NOT here \u2014 that\ngives us HTTP 400 via ``ValidationError`` instead of Pydantic's 422,\nper ACCEL-358 acceptance criteria.", "properties": {"data": {"description": "List of student items to create.", "items": {"$ref": "#/components/schemas/_StudentCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "StudentCreateRequestEnvelope", "type": "object"}, "_StudentCreateData": {"description": "JSON:API data wrapper for student creation.", "properties": {"type": {"const": "students", "description": "Must be \"students\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/StudentCreateRequest"}}, "required": ["type", "attributes"], "title": "_StudentCreateData", "type": "object"}, "StudentUpdateRequest": {"description": "Request attributes for PATCH /v2/students/.\n\nUpdatable fields are ``first_name``, ``last_name``, and ``is_inactive``.\nAll are optional individually, but at least one must be provided per item.\nThe ``email`` field is read-only \u2014 see field description.", "properties": {"email": {"anyOf": [{"format": "email", "type": "string"}, {"type": "null"}], "description": "Student's email address. Read-only via this endpoint \u2014 serves as identifier when ``id`` is absent, or confirmation when ``id`` is present. Use a separate workflow to change email.", "title": "Email"}, "first_name": {"anyOf": [{"maxLength": 50, "type": "string"}, {"type": "null"}], "description": "Student's given name. Maximum 50 characters.", "title": "First Name"}, "last_name": {"anyOf": [{"maxLength": 50, "type": "string"}, {"type": "null"}], "description": "Student's family name. Maximum 50 characters.", "title": "Last Name"}, "is_inactive": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Profile inactive flag. True deactivates; False reactivates. Does NOT touch enrollments.", "title": "Is Inactive"}}, "title": "StudentUpdateRequest", "type": "object"}, "StudentUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/students/ batch update.\n\nThe ``data`` field is always an array \u2014 a single-row update is a list of\none. Cap enforcement (<=DEFAULT_BATCH_MAX_ITEMS items) lives in the\nendpoint, NOT here \u2014 that gives us HTTP 400 via ``ValidationError``\ninstead of Pydantic's 422.", "properties": {"data": {"description": "List of student items to update.", "items": {"$ref": "#/components/schemas/_StudentUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "StudentUpdateRequestEnvelope", "type": "object"}, "_StudentUpdateData": {"description": "JSON:API data wrapper for student update.\n\nIdentifier rules (enforced by ``require_identifier``):\n- ``id`` (obfuscated resource id) is preferred when present.\n- If ``id`` is absent, ``attributes.email`` serves as the identifier and\n  cannot also be updated in the same item.\n- At least one of ``id`` or ``attributes.email`` is required.", "properties": {"type": {"const": "students", "description": "Must be \"students\".", "title": "Type", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated student resource id. Required unless attributes.email identifies the row.", "title": "Id"}, "attributes": {"$ref": "#/components/schemas/StudentUpdateRequest"}}, "required": ["type", "attributes"], "title": "_StudentUpdateData", "type": "object"}, "StudentDetailResponse": {"description": "JSON:API single-resource response for a student.", "properties": {"data": {"$ref": "#/components/schemas/StudentResource"}}, "required": ["data"], "title": "StudentDetailResponse", "type": "object"}, "SetPasswordAttributes": {"description": "Attributes for POST /v2/students/{id}/set-password/.", "properties": {"password": {"description": "New password for the student. Must meet the organization's password policy.", "title": "Password", "type": "string"}}, "required": ["password"], "title": "SetPasswordAttributes", "type": "object"}, "SetPasswordRequestEnvelope": {"description": "JSON:API envelope for POST /v2/students/{id}/set-password/ requests.", "properties": {"data": {"$ref": "#/components/schemas/_SetPasswordData"}}, "required": ["data"], "title": "SetPasswordRequestEnvelope", "type": "object"}, "_SetPasswordData": {"description": "JSON:API data wrapper for set-password.", "properties": {"type": {"const": "password-sets", "description": "Must be \"password-sets\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/SetPasswordAttributes"}}, "required": ["type", "attributes"], "title": "_SetPasswordData", "type": "object"}, "LessonFilters": {"description": "Query parameters for GET /v2/lessons/.\n\nPagination (``page[size]`` / ``page[cursor]``) is read directly from\nthe request by ``CursorPaginator`` and is not declared here. Mirrors\nthe ``CourseFilters`` convention. Declaring legacy ``limit`` / ``cursor``\naliases would advertise param names that disagree with the canonical\nnames emitted in ``links.next`` (always ``page[cursor]``), so SDK\ngenerators were producing misleading clients.", "properties": {"filter[course_id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent course ID. Exact match.", "title": "Filter[Course Id]"}, "filter[type]": {"anyOf": [{"enum": ["ASSET", "HTML", "QUIZ", "WEB_PACKAGE", "VILT", "IE_EXAM", "WIDGET", "MODULAR"], "type": "string"}, {"type": "null"}], "description": "Lesson type. Exact match. Accepted values: ASSET, HTML, QUIZ, WEB_PACKAGE, VILT, IE_EXAM, WIDGET, MODULAR. Unknown values are rejected with 422.", "title": "Filter[Type]"}, "filter[title]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Lesson title. Case-insensitive exact match.", "title": "Filter[Title]"}, "filter[updated_since]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns lessons updated at or after this time. Naive datetimes without an offset are rejected with 422.", "title": "Filter[Updated Since]"}}, "title": "LessonFilters", "type": "object"}, "LessonAttributes": {"description": "Attributes of a lesson resource object.", "properties": {"title": {"description": "Lesson title.", "title": "Title", "type": "string"}, "type": {"description": "Lesson type code (e.g. HTML).", "title": "Type", "type": "string"}, "order": {"description": "Ordering value within the parent course.", "title": "Order", "type": "integer"}, "course_id": {"description": "Obfuscated ID of the parent course.", "title": "Course Id", "type": "string"}, "description_html": {"default": "", "description": "HTML description shown above the lesson body.", "title": "Description Html", "type": "string"}, "content_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "HTML body of the lesson (when type=\"HTML\").", "title": "Content Html"}, "quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated ID of the referenced quiz (when type=\"QUIZ\").", "title": "Quiz Id"}, "external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "created_at": {"description": "Timestamp when the lesson was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the lesson was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["title", "type", "order", "course_id", "external_id", "created_at", "modified_at"], "title": "LessonAttributes", "type": "object"}, "LessonResource": {"description": "JSON:API resource object for a lesson.", "properties": {"type": {"const": "lessons", "default": "lessons", "description": "Always \"lessons\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque lesson ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/LessonAttributes"}, "relationships": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Related resource links.", "title": "Relationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "LessonResource", "type": "object"}, "PaginatedListResponse_LessonResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/LessonResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[LessonResource]", "type": "object"}, "BatchResultEnvelope_LessonDetailResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_LessonDetailResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[LessonDetailResource]", "type": "object"}, "BatchSucceededItem_LessonDetailResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/LessonDetailResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[LessonDetailResource]", "type": "object"}, "ContentItemResource": {"description": "Read representation of a MODULAR lesson's content block.\n\nPopulated by the detail/create/update builder (never on the list route).\n``rating`` echoes the effective course ``rating_scale`` for RATING items;\nunused type fields serialize as null and are dropped by the endpoint's\n``exclude_none``.", "properties": {"id": {"description": "Obfuscated content-item id.", "title": "Id", "type": "string"}, "type": {"description": "Content item type code.", "title": "Type", "type": "string"}, "header": {"default": "", "description": "Header shown above the block.", "title": "Header", "type": "string"}, "content_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "HTML body (when type is \"HTML\").", "title": "Content Html"}, "asset_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated asset id (when type is \"VIDEO\"/\"AUDIO\").", "title": "Asset Id"}, "quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated quiz id (when type is \"QUIZ\").", "title": "Quiz Id"}, "rating": {"anyOf": [{"$ref": "#/components/schemas/RatingConfig"}, {"type": "null"}], "description": "Rating configuration (when type is \"RATING\")."}}, "required": ["id", "type"], "title": "ContentItemResource", "type": "object"}, "LessonDetailAttributes": {"description": "Attributes of a lesson resource on the detail/create/update responses.\n\nExtends the list attributes with the MODULAR ``content_items`` array. Kept\noff the shared ``LessonAttributes`` so the **list** response stays\nbyte-identical (an Optional ``content_items`` on the list type would\nserialize as ``content_items: null`` \u2014 the list route has no\n``exclude_none``). ``None`` for non-MODULAR lessons; the detail/create/update\nroutes set ``exclude_none`` so the field is omitted then.", "properties": {"title": {"description": "Lesson title.", "title": "Title", "type": "string"}, "type": {"description": "Lesson type code (e.g. HTML).", "title": "Type", "type": "string"}, "order": {"description": "Ordering value within the parent course.", "title": "Order", "type": "integer"}, "course_id": {"description": "Obfuscated ID of the parent course.", "title": "Course Id", "type": "string"}, "description_html": {"default": "", "description": "HTML description shown above the lesson body.", "title": "Description Html", "type": "string"}, "content_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "HTML body of the lesson (when type=\"HTML\").", "title": "Content Html"}, "quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated ID of the referenced quiz (when type=\"QUIZ\").", "title": "Quiz Id"}, "external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "created_at": {"description": "Timestamp when the lesson was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the lesson was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}, "content_items": {"anyOf": [{"items": {"$ref": "#/components/schemas/ContentItemResource"}, "type": "array"}, {"type": "null"}], "description": "Ordered content blocks. Present only when type is \"MODULAR\".", "title": "Content Items"}}, "required": ["title", "type", "order", "course_id", "external_id", "created_at", "modified_at"], "title": "LessonDetailAttributes", "type": "object"}, "LessonDetailResource": {"description": "JSON:API resource object for a lesson on the detail/create/update path.\n\nCarries ``content_items`` (via ``LessonDetailAttributes``). The **list**\nroute keeps returning the plain ``LessonResource`` so its shape is unchanged.", "properties": {"type": {"const": "lessons", "default": "lessons", "description": "Always \"lessons\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque lesson ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/LessonDetailAttributes"}, "relationships": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Related resource links.", "title": "Relationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "LessonDetailResource", "type": "object"}, "RatingConfig": {"additionalProperties": false, "description": "Rating configuration carried by a ``RATING`` content item.\n\n``rating_scale`` is a **proxy** for the course-level ``Course.rating_scale``\n(not a ``RatingContentBlock`` column), mirroring\n``api/v1/lessons/rating_content_blocks/serializers.py``. Constrained to\n``5`` / ``10`` at the type level so an out-of-range value (e.g. ``7``) is a\n422; the 10-point capability gate is enforced in the service. Unknown keys\nare rejected (422) so a typo'd rating field never silently no-ops.\n\nReused as the read shape on ``ContentItemResource``: the builder echoes the\neffective course ``rating_scale`` and omits the low/high labels when the\n10-point flag is off (relying on the endpoint's ``exclude_none``).", "properties": {"rating_question": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Main rating question shown to learners.", "title": "Rating Question"}, "additional_feedback_question": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional follow-up feedback question.", "title": "Additional Feedback Question"}, "include_additional_feedback": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether to show the feedback question.", "title": "Include Additional Feedback"}, "rating_scale_low_label": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Low-end label (10-point scale only).", "title": "Rating Scale Low Label"}, "rating_scale_high_label": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "High-end label (10-point scale only).", "title": "Rating Scale High Label"}, "rating_scale": {"anyOf": [{"enum": [5, 10], "type": "integer"}, {"type": "null"}], "description": "Rating scale (5 or 10). Course-level proxy \u2014 stored on Course, not the block.", "title": "Rating Scale"}}, "title": "RatingConfig", "type": "object"}, "ContentItemRequest": {"description": "A single ordered content block within a MODULAR lesson's write payload.\n\nEach item carries exactly one type-specific payload field matching its\n``type``; supplying a foreign field (e.g. ``quiz_id`` on an ``HTML`` item)\nis a 422. The optional obfuscated ``id`` is the diff key: present = update\nthe existing child, omitted = create a new one.", "properties": {"id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated content-item id. Present = update existing child; omitted = create new.", "title": "Id"}, "type": {"description": "Content item type.", "enum": ["HTML", "VIDEO", "AUDIO", "QUIZ", "RATING"], "title": "Type", "type": "string"}, "header": {"default": "", "description": "Optional header shown above the block.", "maxLength": 500, "title": "Header", "type": "string"}, "content_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "HTML body. Only valid when type is \"HTML\".", "title": "Content Html"}, "asset_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated asset id. Only valid when type is \"VIDEO\" or \"AUDIO\".", "title": "Asset Id"}, "quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated quiz id. Only valid when type is \"QUIZ\".", "title": "Quiz Id"}, "rating": {"anyOf": [{"$ref": "#/components/schemas/RatingConfig"}, {"type": "null"}], "description": "Rating configuration. Only valid when type is \"RATING\"."}}, "required": ["type"], "title": "ContentItemRequest", "type": "object"}, "LessonCreateRequest": {"description": "Request schema for creating a new lesson.\n\n``type`` admits ``HTML``, ``MODULAR``, and ``QUIZ``. An HTML lesson carries\nits body in ``content_html``; a MODULAR lesson carries an ordered\n``content_items`` array and forbids lesson-level ``content_html``; a QUIZ\nlesson references an existing quiz via ``quiz_id`` and forbids both\n``content_html`` and ``content_items``. The schema's ``Literal`` is the\nsingle, reviewable gate \u2014 the service layer is permissive.", "properties": {"course_id": {"description": "Obfuscated ID of the parent course.", "title": "Course Id", "type": "string"}, "type": {"description": "Lesson type. One of \"HTML\", \"MODULAR\", or \"QUIZ\".", "enum": ["HTML", "MODULAR", "QUIZ"], "title": "Type", "type": "string"}, "title": {"description": "Lesson title.", "maxLength": 500, "title": "Title", "type": "string"}, "description_html": {"default": "", "description": "Optional HTML description shown above the lesson body.", "title": "Description Html", "type": "string"}, "content_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "HTML body of the lesson. Required and must be non-empty when ``type`` is \"HTML\"; forbidden when ``type`` is \"MODULAR\". The underlying model column permits empty strings, but the API rejects empty content on create because an HTML lesson with no body is not a useful resource.", "title": "Content Html"}, "content_items": {"anyOf": [{"items": {"$ref": "#/components/schemas/ContentItemRequest"}, "maxItems": 15, "type": "array"}, {"type": "null"}], "description": "Ordered content blocks. Valid only when ``type`` is \"MODULAR\". Capped at 15 items; at most one QUIZ and one RATING item.", "title": "Content Items"}, "quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated ID of the quiz this lesson references. Required and must be non-empty when ``type`` is \"QUIZ\"; forbidden for every other type.", "title": "Quiz Id"}, "order": {"anyOf": [{"maximum": 2147483647, "minimum": 0, "type": "integer"}, {"type": "null"}], "description": "Optional ordering value within the parent course. Defaults to max(existing) + 10 when omitted.", "title": "Order"}}, "required": ["course_id", "type", "title"], "title": "LessonCreateRequest", "type": "object"}, "LessonCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/lessons/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<=100 items) lives in the endpoint, NOT here \u2014 that\ngives us HTTP 400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of lesson items to create.", "items": {"$ref": "#/components/schemas/_LessonCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "LessonCreateRequestEnvelope", "type": "object"}, "_LessonCreateData": {"description": "JSON:API data wrapper for lesson creation.", "properties": {"type": {"const": "lessons", "description": "Must be \"lessons\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/LessonCreateRequest"}}, "required": ["type", "attributes"], "title": "_LessonCreateData", "type": "object"}, "LessonBatchUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/lessons/ batch updates.\n\nThe ``data`` field is always an array. Cap enforcement (<=100 items) lives\nin the endpoint so violations produce HTTP 400 rather than Pydantic 422.", "properties": {"data": {"description": "List of lesson update items.", "items": {"$ref": "#/components/schemas/_LessonBatchUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "LessonBatchUpdateRequestEnvelope", "type": "object"}, "LessonUpdateRequest": {"description": "Request schema for partial updates of a lesson.\n\nFields are typed as non-Optional so Pydantic rejects explicit ``null``\nat the type level. PATCH semantics are preserved by Python-level\nsentinel defaults combined with ``model_fields_set``: an omitted field\nhas its default value but is not in ``model_fields_set``, so callers\nuse ``model_dump(exclude_unset=True)`` to forward only fields the\nclient explicitly provided. The sentinel default is stripped from the\nemitted JSON Schema so SDK generators do not advertise it to clients.\n\nThe lesson ``type`` field is read-only on update and is intentionally\nnot part of this schema; clients that send it have it silently ignored.", "properties": {"title": {"description": "New lesson title. Must be non-empty when provided.", "maxLength": 500, "title": "Title", "type": "string"}, "description_html": {"description": "New HTML description shown above the lesson body. Empty string ``\"\"`` is accepted and clears the description, mirroring the create-side default. This is intentional asymmetry with ``title`` and ``content_html`` (which reject empty/whitespace) \u2014 the description is genuinely optional, the title and body are not.", "title": "Description Html", "type": "string"}, "content_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "New HTML body of the lesson. Only valid on HTML-type lessons; attempting to set this on a non-HTML lesson returns 400.", "title": "Content Html"}, "order": {"description": "New ordering value within the parent course. Order conflicts with sibling lessons are not auto-resolved: if the requested value collides with another lesson in the same course, the update succeeds and both lessons retain the same value; display order in that case is undefined.", "maximum": 2147483647, "minimum": 0, "title": "Order", "type": "integer"}, "content_items": {"anyOf": [{"items": {"$ref": "#/components/schemas/ContentItemRequest"}, "maxItems": 15, "type": "array"}, {"type": "null"}], "description": "Authoritative ordered content blocks for a MODULAR lesson. Omitted = children untouched; present non-empty = diff/reorder; present ``[]`` = delete all children. Capped at 15 items; at most one QUIZ and one RATING item. Valid only on MODULAR lessons \u2014 the cross-state MODULAR gate is enforced by the service.", "title": "Content Items"}, "quiz_id": {"description": "New quiz reference for a QUIZ lesson. Must be non-empty when provided; re-points the lesson at a different quiz. There is no null-clear affordance \u2014 a QUIZ lesson's quiz cannot be removed via PATCH. Only valid on QUIZ lessons; the cross-state QUIZ gate is enforced by the service.", "title": "Quiz Id", "type": "string"}}, "title": "LessonUpdateRequest", "type": "object"}, "_LessonBatchUpdateData": {"description": "JSON:API data wrapper for a single item in a batch lesson update.", "properties": {"type": {"const": "lessons", "description": "Must be \"lessons\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the lesson to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/LessonUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_LessonBatchUpdateData", "type": "object"}, "LessonDetailResponse": {"description": "Single-resource envelope for GET /v2/lessons/{id}/ responses.", "properties": {"data": {"$ref": "#/components/schemas/LessonDetailResource"}}, "required": ["data"], "title": "LessonDetailResponse", "type": "object"}, "CourseFilters": {"description": "Query parameters for GET /v2/courses.\n\nRedeclares ``title`` with the ``filter[title]`` alias so the HTTP surface\nonly accepts the JSON:API bracketed form. Pagination (``page[size]`` /\n``page[cursor]``) is read directly from the request by ``CursorPaginator``\nand is not declared here.", "properties": {"filter[title]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Case-insensitive partial match on course title.", "title": "Filter[Title]"}}, "title": "CourseFilters", "type": "object"}, "CourseAttributes": {"description": "Attributes of a course resource object.", "properties": {"external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "title": {"description": "Course title.", "title": "Title", "type": "string"}, "short_description": {"description": "Short description shown on course tiles.", "title": "Short Description", "type": "string"}, "long_description_html": {"description": "Long description as HTML content.", "title": "Long Description Html", "type": "string"}, "enforce_sequential_navigation": {"description": "If true, students must complete the current lesson before accessing the next.", "title": "Enforce Sequential Navigation", "type": "boolean"}, "lesson_count": {"description": "Total number of lessons in the course.", "title": "Lesson Count", "type": "integer"}, "is_published": {"description": "True if the course is published on at least one live domain.", "title": "Is Published", "type": "boolean"}, "promo_image_url": {"description": "Promo image URL, or \"\" if no image is set.", "title": "Promo Image Url", "type": "string"}, "average_rating": {"anyOf": [{"type": "number"}, {"type": "null"}], "description": "Average of all student ratings on the org's active rating scale, or null when no ratings exist.", "title": "Average Rating"}, "created_at": {"description": "Course creation timestamp.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Latest update timestamp \u2014 max of the course's updated_at and any lesson's updated_at.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["external_id", "title", "short_description", "long_description_html", "enforce_sequential_navigation", "lesson_count", "is_published", "promo_image_url", "created_at", "modified_at"], "title": "CourseAttributes", "type": "object"}, "CourseResource": {"description": "JSON:API resource object for a course.", "properties": {"type": {"default": "courses", "description": "Always \"courses\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque course ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/CourseAttributes"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "CourseResource", "type": "object"}, "PaginatedListResponse_CourseResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/CourseResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[CourseResource]", "type": "object"}, "BatchResultEnvelope_CourseResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_CourseResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[CourseResource]", "type": "object"}, "BatchSucceededItem_CourseResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/CourseResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[CourseResource]", "type": "object"}, "CourseCreateRequest": {"description": "Request attributes for POST /v2/courses/.", "properties": {"title": {"description": "Course title.", "maxLength": 500, "minLength": 1, "title": "Title", "type": "string"}, "short_description": {"default": "", "description": "Short description shown on course tiles.", "maxLength": 500, "title": "Short Description", "type": "string"}, "long_description_html": {"default": "", "description": "Long description as HTML content.", "title": "Long Description Html", "type": "string"}, "enforce_sequential_navigation": {"default": false, "description": "If true, students must complete the current lesson before accessing the next.", "title": "Enforce Sequential Navigation", "type": "boolean"}, "created_by_email": {"anyOf": [{"format": "email", "type": "string"}, {"type": "null"}], "description": "Optional dashboard-user email to attribute the create to. Resolved to an active OrganizationMembership in the caller's organization. Omitted/null leaves created_by null.", "title": "Created By Email"}}, "required": ["title"], "title": "CourseCreateRequest", "type": "object"}, "CourseCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/courses/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<= ``BATCH_MAX_ITEMS`` items) lives in the endpoint, NOT\nhere \u2014 that gives us HTTP 400 via ``ValidationError`` instead of Pydantic's\n422, per the v2 batch-response convention.", "properties": {"data": {"description": "List of course items to create.", "items": {"$ref": "#/components/schemas/_CourseCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "CourseCreateRequestEnvelope", "type": "object"}, "_CourseCreateData": {"description": "JSON:API data wrapper for course creation.", "properties": {"type": {"const": "courses", "description": "Must be \"courses\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/CourseCreateRequest"}}, "required": ["type", "attributes"], "title": "_CourseCreateData", "type": "object"}, "CourseUpdateRequest": {"description": "Request attributes for PATCH /v2/courses/.", "properties": {"title": {"anyOf": [{"maxLength": 500, "type": "string"}, {"type": "null"}], "description": "Course title.", "title": "Title"}, "short_description": {"anyOf": [{"maxLength": 500, "type": "string"}, {"type": "null"}], "description": "Short description shown on course tiles.", "title": "Short Description"}, "long_description_html": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Long description as HTML content.", "title": "Long Description Html"}, "enforce_sequential_navigation": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "If true, students must complete the current lesson before accessing the next.", "title": "Enforce Sequential Navigation"}}, "title": "CourseUpdateRequest", "type": "object"}, "CourseUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/courses/.\n\nThe ``data`` field is always an array \u2014 single-update is a list of one.\nCap enforcement (<= ``BATCH_MAX_ITEMS`` items) lives in the endpoint, matching\nthe create-endpoint convention.", "properties": {"data": {"description": "List of course items to update.", "items": {"$ref": "#/components/schemas/_CourseUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "CourseUpdateRequestEnvelope", "type": "object"}, "_CourseUpdateData": {"description": "JSON:API data wrapper for course update.", "properties": {"type": {"const": "courses", "description": "Must be \"courses\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the course to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/CourseUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_CourseUpdateData", "type": "object"}, "CourseDetailResponse": {"description": "JSON:API single-resource response for a course.", "properties": {"data": {"$ref": "#/components/schemas/CourseResource"}}, "required": ["data"], "title": "CourseDetailResponse", "type": "object"}, "CourseAnalyticsFilters": {"description": "Query parameters for GET /v2/analytics/courses/{course_id}/.", "properties": {"filter[domains]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; counts only enrollments whose published course is in one of those domains. Unknown names match nothing, never error.", "title": "Filter[Domains]"}}, "title": "CourseAnalyticsFilters", "type": "object"}, "CourseAnalyticsAttributes": {"description": "Attributes of a course-analytics resource.", "properties": {"enrollments": {"description": "Number of enrollment rows across the course's published courses.", "title": "Enrollments", "type": "integer"}, "completions": {"description": "Number of those enrollments whose course progress is completed.", "title": "Completions", "type": "integer"}, "completion_rate": {"anyOf": [{"type": "number"}, {"type": "null"}], "description": "Completions divided by enrollments, or null when there are no enrollments.", "title": "Completion Rate"}, "average_rating": {"anyOf": [{"type": "number"}, {"type": "null"}], "description": "Average student rating on the org's active rating scale, or null when there are no ratings.", "title": "Average Rating"}, "rating_scale": {"description": "The org's active rating scale used for rating aggregates (5 or 10).", "title": "Rating Scale", "type": "integer"}, "rating_count": {"description": "Number of ratings counted on the org's active rating scale.", "title": "Rating Count", "type": "integer"}, "certificates_issued": {"description": "Number of those enrollments with a certificate.", "title": "Certificates Issued", "type": "integer"}}, "required": ["enrollments", "completions", "rating_scale", "rating_count", "certificates_issued"], "title": "CourseAnalyticsAttributes", "type": "object"}, "CourseAnalyticsDetailResponse": {"description": "JSON:API single-resource response for a course's analytics summary.", "properties": {"data": {"$ref": "#/components/schemas/CourseAnalyticsResource"}}, "required": ["data"], "title": "CourseAnalyticsDetailResponse", "type": "object"}, "CourseAnalyticsRelationships": {"description": "Relationships of a course-analytics resource.", "properties": {"course": {"$ref": "#/components/schemas/CourseRelationship"}}, "required": ["course"], "title": "CourseAnalyticsRelationships", "type": "object"}, "CourseAnalyticsResource": {"description": "JSON:API resource object for a course's analytics summary.", "properties": {"type": {"default": "course-analytics", "description": "Always \"course-analytics\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque course ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/CourseAnalyticsAttributes"}, "relationships": {"$ref": "#/components/schemas/CourseAnalyticsRelationships"}}, "required": ["id", "attributes", "relationships"], "title": "CourseAnalyticsResource", "type": "object"}, "CourseIdentifier": {"description": "JSON:API resource identifier for a course.", "properties": {"type": {"const": "courses", "description": "Must be \"courses\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque course id.", "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "CourseIdentifier", "type": "object"}, "CourseRelationship": {"description": "To-one linkage to the course being published.", "properties": {"data": {"$ref": "#/components/schemas/CourseIdentifier"}}, "required": ["data"], "title": "CourseRelationship", "type": "object"}, "CourseRatingFilters": {"description": "Query parameters for GET /v2/analytics/courses/{course_id}/ratings/.", "properties": {"filter[student.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only ratings by the student with this opaque id. Unknown or malformed ids match nothing.", "title": "Filter[Student.Id]"}}, "title": "CourseRatingFilters", "type": "object"}, "CourseRatingAttributes": {"description": "Attributes of a course-rating resource.", "properties": {"rating": {"description": "The student's rating value on the rating's scale.", "title": "Rating", "type": "integer"}, "scale_max": {"description": "The maximum value of the rating's scale (5 or 10).", "title": "Scale Max", "type": "integer"}, "feedback": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The student's additional feedback text, or null when none.", "title": "Feedback"}, "created_at": {"description": "When the rating was submitted.", "format": "date-time", "title": "Created At", "type": "string"}}, "required": ["rating", "scale_max", "created_at"], "title": "CourseRatingAttributes", "type": "object"}, "CourseRatingRelationships": {"description": "Relationships of a course-rating resource.", "properties": {"student": {"$ref": "#/components/schemas/StudentRelationship"}, "course": {"$ref": "#/components/schemas/CourseRelationship"}}, "required": ["student", "course"], "title": "CourseRatingRelationships", "type": "object"}, "CourseRatingResource": {"description": "JSON:API resource object for one of a course's ratings.", "properties": {"type": {"default": "course-ratings", "description": "Always \"course-ratings\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque rating ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/CourseRatingAttributes"}, "relationships": {"$ref": "#/components/schemas/CourseRatingRelationships"}}, "required": ["id", "attributes", "relationships"], "title": "CourseRatingResource", "type": "object"}, "PaginatedListResponse_CourseRatingResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/CourseRatingResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[CourseRatingResource]", "type": "object"}, "StudentIdentifier": {"description": "JSON:API resource identifier for a student.", "properties": {"type": {"default": "students", "description": "Always \"students\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque student ID.", "title": "Id", "type": "string"}}, "required": ["id"], "title": "StudentIdentifier", "type": "object"}, "StudentRelationship": {"description": "To-one linkage to a student; ``data`` is null when there is no student.", "properties": {"data": {"anyOf": [{"$ref": "#/components/schemas/StudentIdentifier"}, {"type": "null"}]}}, "title": "StudentRelationship", "type": "object"}, "CertificateAttributes": {"description": "Attributes of a certificate resource.", "properties": {"code": {"description": "Unique code identifying the certificate.", "title": "Code", "type": "string"}, "issued_at": {"description": "Timestamp the certificate was issued.", "format": "date-time", "title": "Issued At", "type": "string"}, "expires_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp the certificate expires, or null when it does not expire.", "title": "Expires At"}, "score_as_percent": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Student's course score as a percentage, or null when not recorded.", "title": "Score As Percent"}}, "required": ["code", "issued_at"], "title": "CertificateAttributes", "type": "object"}, "CertificateDetailResponse": {"description": "JSON:API single-resource response for GET /v2/certificates/{id}/.", "properties": {"data": {"$ref": "#/components/schemas/CertificateResource"}}, "required": ["data"], "title": "CertificateDetailResponse", "type": "object"}, "CertificateRelationships": {"description": "Relationships of a certificate resource.", "properties": {"student": {"$ref": "#/components/schemas/StudentRelationship"}, "course": {"$ref": "#/components/schemas/CourseRelationship"}}, "required": ["student", "course"], "title": "CertificateRelationships", "type": "object"}, "CertificateResource": {"description": "JSON:API resource object for a certificate.", "properties": {"type": {"default": "certificates", "description": "Always \"certificates\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque certificate ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/CertificateAttributes"}, "relationships": {"$ref": "#/components/schemas/CertificateRelationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self link.", "title": "Links"}}, "required": ["id", "attributes", "relationships"], "title": "CertificateResource", "type": "object"}, "CertificateListQuery": {"description": "Query parameters for GET /v2/certificates/.", "properties": {"filter[domains]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; matches any listed domain. Unknown names match nothing.", "title": "Filter[Domains]"}, "filter[status]": {"allOf": [{"description": "Expiry-state filter values for GET /v2/certificates.", "enum": ["active", "expired", "all"], "title": "CertificateStatus", "type": "string"}], "default": "all", "description": "Filter by expiry state: active (unexpired), expired, or all. Defaults to all."}, "filter[issued_gte]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only certificates issued at or after this ISO 8601 timestamp.", "title": "Filter[Issued Gte]"}, "filter[issued_lte]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only certificates issued at or before this ISO 8601 timestamp.", "title": "Filter[Issued Lte]"}, "filter[student.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only certificates for the student with this opaque id. Unknown ids match nothing.", "title": "Filter[Student.Id]"}, "filter[course.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only certificates for the course with this opaque id. Unknown ids match nothing.", "title": "Filter[Course.Id]"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "CertificateListQuery", "type": "object"}, "CertificateStatus": {"description": "Expiry-state filter values for GET /v2/certificates.", "enum": ["active", "expired", "all"], "title": "CertificateStatus", "type": "string"}, "PaginatedListResponse_CertificateResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/CertificateResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[CertificateResource]", "type": "object"}, "GroupFilters": {"description": "Query parameters for GET /v2/groups.", "properties": {"filter[name]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Case-insensitive substring match on group name. Backed by a sequential scan inside the caller's organization; expected typical org cardinality is < 1k groups.", "title": "Filter[Name]"}, "filter[category_id]": {"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}], "description": "Obfuscated id of a StudentGroupCategory. Returns only groups in that category. Undecodable or cross-organization ids return zero results, never an error.", "title": "Filter[Category Id]"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "GroupFilters", "type": "object"}, "GroupAttributes": {"description": "Attributes of a group resource object.", "properties": {"name": {"description": "Group name.", "title": "Name", "type": "string"}, "rule_email_domains": {"description": "Email domains that auto-add students to this group on signup.", "items": {"type": "string"}, "title": "Rule Email Domains", "type": "array"}, "send_course_enrollment_email": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether to send course enrollment emails to members of this group.", "title": "Send Course Enrollment Email"}, "category_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated id of the assigned StudentGroupCategory, or null if no category is set.", "title": "Category Id"}, "created_at": {"description": "Timestamp when the group was created.", "format": "date-time", "title": "Created At", "type": "string"}, "updated_at": {"description": "Timestamp when the group was last updated.", "format": "date-time", "title": "Updated At", "type": "string"}}, "required": ["name", "rule_email_domains", "created_at", "updated_at"], "title": "GroupAttributes", "type": "object"}, "GroupResource": {"description": "JSON:API resource object for a group.", "properties": {"type": {"const": "groups", "default": "groups", "description": "Always \"groups\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque group ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/GroupAttributes"}}, "required": ["id", "attributes"], "title": "GroupResource", "type": "object"}, "PaginatedListResponse_GroupResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/GroupResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[GroupResource]", "type": "object"}, "BatchResultEnvelope_GroupResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_GroupResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[GroupResource]", "type": "object"}, "BatchSucceededItem_GroupResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/GroupResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[GroupResource]", "type": "object"}, "GroupCreateRequest": {"description": "Request schema for creating a new group.", "properties": {"name": {"description": "Group name. Must be unique within the organization (case-sensitive).", "maxLength": 100, "minLength": 1, "title": "Name", "type": "string"}, "rule_email_domains": {"description": "Email domains that auto-add students to this group on signup. Each entry is a bare domain (e.g. ``example.com``); duplicates are collapsed.", "items": {"type": "string"}, "title": "Rule Email Domains", "type": "array"}, "send_course_enrollment_email": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether to send course enrollment emails to members of this group.", "title": "Send Course Enrollment Email"}, "category_id": {"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}], "description": "Obfuscated id of an existing StudentGroupCategory. The category must belong to the same organization or the item fails with ``not_found``.", "title": "Category Id"}}, "required": ["name"], "title": "GroupCreateRequest", "type": "object"}, "GroupCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/groups/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<= DEFAULT_BATCH_MAX_ITEMS items) lives in the endpoint,\nNOT here \u2014 that gives us HTTP 400 via ``ValidationError`` instead of\nPydantic's 422, matching the contract established by ACCEL-358 / ACCEL-359.", "properties": {"data": {"description": "List of group items to create.", "items": {"$ref": "#/components/schemas/_GroupCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "GroupCreateRequestEnvelope", "type": "object"}, "_GroupCreateData": {"description": "JSON:API data wrapper for group creation.", "properties": {"type": {"const": "groups", "description": "Must be \"groups\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/GroupCreateRequest"}}, "required": ["type", "attributes"], "title": "_GroupCreateData", "type": "object"}, "GroupUpdateRequest": {"description": "Request attributes for PATCH /v2/groups/.\n\nUpdatable fields are ``name``, ``rule_email_domains``,\n``send_course_enrollment_email``, and ``category_id``. All are optional\nindividually; at least one must be provided per item.\n\n``category_id`` may be set to ``null`` explicitly to clear the category\nassignment. Distinguishing \"not provided\" from \"explicitly null\" uses\nPydantic's ``model_fields_set`` (see ``_GroupUpdateData``).", "properties": {"name": {"anyOf": [{"maxLength": 100, "minLength": 1, "type": "string"}, {"type": "null"}], "description": "New group name. Must remain unique within the organization (case-sensitive).", "title": "Name"}, "rule_email_domains": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Replacement list of auto-add email domains. Replaces the entire array.", "title": "Rule Email Domains"}, "send_course_enrollment_email": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether to send course enrollment emails to members of this group.", "title": "Send Course Enrollment Email"}, "category_id": {"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}], "description": "Obfuscated id of a StudentGroupCategory to assign, or ``null`` to clear the assignment. Distinguishing 'not provided' from 'explicit null' uses ``model_fields_set``.", "title": "Category Id"}}, "title": "GroupUpdateRequest", "type": "object"}, "GroupUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/groups/ batch update.", "properties": {"data": {"description": "List of group items to update.", "items": {"$ref": "#/components/schemas/_GroupUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "GroupUpdateRequestEnvelope", "type": "object"}, "_GroupUpdateData": {"description": "JSON:API data wrapper for group update.\n\nIdentifier rules:\n- ``id`` (obfuscated resource id) is REQUIRED. Groups do not have a\n  stable natural identifier the way students have email \u2014 names can\n  be renamed and are not durable cross-call identifiers.", "properties": {"type": {"const": "groups", "description": "Must be \"groups\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated group resource id.", "maxLength": 64, "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/GroupUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_GroupUpdateData", "type": "object"}, "BatchDeleteResultEnvelope": {"description": "207 Multi-Status response envelope for batch-delete operations.\n\nDistinct from ``BatchResultEnvelope`` so the generic ``T`` on the latter\nstays bound to a real resource type and consumers reading\n``BatchSucceededItem.result`` keep their non-null contract.\n\nItems appear in the same order as the request inputs. ``summary.succeeded``\ncounts ``BatchDeletedItem`` rows \u2014 the field name reflects \"operation did\nnot fail\" rather than the verb-specific outcome (which is on every item's\nown ``status`` discriminator).", "properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchDeletedItem"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchDeleteResultEnvelope", "type": "object"}, "BatchDeletedItem": {"description": "Wrapper for a successfully deleted item in a 207 batch-delete response.\n\nThe ``status`` discriminator is ``\"deleted\"`` (not ``\"succeeded\"``) so the\noutcome is explicit on every row without inspecting the rest of the\nobject. There is no ``result`` field: the row has been removed and has no\ncurrent resource representation to echo.\n\n``id`` semantics depend on the endpoint:\n\n- **Resource-keyed deletes** (e.g. ``DELETE /v2/groups/``): the deleted\n  resource's own obfuscated id, echoed from the request.\n- **Relationship sub-resource deletes** (e.g. ``DELETE\n  /v2/groups/{id}/relationships/published-course-visibility/``): the\n  related-resource id (here, ``published_course_id``), echoed from the\n  request. The deleted ``PublishedCourseGroupVisibility`` row's own\n  obfuscated id is not surfaced because clients identify these rows by\n  the relationship tuple, not by their own id.\n\nSee ``api_v2/docs/groups.md`` \u00a7\"Visibility overrides (relationship\nsub-resource)\" for the rationale on the relationship variant.", "properties": {"status": {"const": "deleted", "default": "deleted", "title": "Status", "type": "string"}, "id": {"description": "Obfuscated id echoed from the request. For resource-keyed deletes this is the deleted resource's own id; for relationship sub-resource deletes this echoes the related-resource id (e.g. ``published_course_id``).", "title": "Id", "type": "string"}}, "required": ["id"], "title": "BatchDeletedItem", "type": "object"}, "GroupDeleteRequestEnvelope": {"description": "JSON:API envelope for DELETE /v2/groups/ batch delete.\n\nThe ``data`` field is always an array \u2014 single-delete is a list of one.\nCap enforcement (<= DEFAULT_BATCH_MAX_ITEMS items) lives in the endpoint,\nNOT here \u2014 matches the create/update contract (400 via ValidationError\ninstead of Pydantic 422).", "properties": {"data": {"description": "List of group items to delete.", "items": {"$ref": "#/components/schemas/_GroupDeleteData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "GroupDeleteRequestEnvelope", "type": "object"}, "_GroupDeleteData": {"description": "JSON:API data wrapper for group deletion.\n\nDelete items carry only ``type`` and ``id`` \u2014 no ``attributes``. Mirrors\nthe resource-identifier object shape from JSON:API \u00a71.0\n(https://jsonapi.org/format/#document-resource-identifier-objects).", "properties": {"type": {"const": "groups", "description": "Must be \"groups\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated group resource id.", "maxLength": 64, "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "_GroupDeleteData", "type": "object"}, "GroupDetailResponse": {"description": "JSON:API single-resource response for a group.", "properties": {"data": {"$ref": "#/components/schemas/GroupResource"}}, "required": ["data"], "title": "GroupDetailResponse", "type": "object"}, "VisibilityOverrideFilters": {"description": "Query parameters for GET /v2/groups/{id}/relationships/published-course-visibility.\n\nBoth filter fields are natural pivots on the underlying\n``(student_group, published_course, is_visible)`` unique constraint \u2014\nexpected client patterns are \"is this course overridden for this group?\"\n(``filter[published_course_id]``) and \"show me the allowlist (or\nblocklist) entries\" (``filter[is_visible]``).", "properties": {"filter[published_course_id]": {"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}], "description": "Obfuscated id of a PublishedCourse. Returns only overrides targeting that course. Undecodable or cross-organization ids return zero results, never an error.", "title": "Filter[Published Course Id]"}, "filter[is_visible]": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "True returns only allowlist overrides (force visible); False returns only blocklist overrides (force hidden); omitting the filter returns both.", "title": "Filter[Is Visible]"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "VisibilityOverrideFilters", "type": "object"}, "PaginatedListResponse_VisibilityOverrideResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/VisibilityOverrideResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[VisibilityOverrideResource]", "type": "object"}, "VisibilityOverrideAttributes": {"description": "Attributes of a visibility-override resource object.", "properties": {"published_course_id": {"description": "Obfuscated id of the PublishedCourse this override applies to.", "title": "Published Course Id", "type": "string"}, "is_visible": {"description": "True if the course is force-visible to the group; False if force-hidden.", "title": "Is Visible", "type": "boolean"}, "created_at": {"description": "Timestamp when the override was created.", "format": "date-time", "title": "Created At", "type": "string"}, "updated_at": {"description": "Timestamp when the override was last updated.", "format": "date-time", "title": "Updated At", "type": "string"}}, "required": ["published_course_id", "is_visible", "created_at", "updated_at"], "title": "VisibilityOverrideAttributes", "type": "object"}, "VisibilityOverrideResource": {"description": "JSON:API resource object for a visibility override.", "properties": {"type": {"const": "visibility-overrides", "default": "visibility-overrides", "description": "Always \"visibility-overrides\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque visibility-override id.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/VisibilityOverrideAttributes"}}, "required": ["id", "attributes"], "title": "VisibilityOverrideResource", "type": "object"}, "BatchResultEnvelope_VisibilityOverrideResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_VisibilityOverrideResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[VisibilityOverrideResource]", "type": "object"}, "BatchSucceededItem_VisibilityOverrideResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/VisibilityOverrideResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[VisibilityOverrideResource]", "type": "object"}, "VisibilityOverrideAddRequest": {"description": "Request attributes for an item in POST visibility-overrides.\n\n``is_visible`` defaults to ``True`` (allowlist case \u2014 make the course\nvisible to the group despite default-hidden). Setting ``False`` adds a\nblocklist entry (hide the course from this group despite default-visible).\nThe underlying model supports both via its\n``(student_group, published_course, is_visible)`` unique constraint, so\n``(group, course, True)`` and ``(group, course, False)`` can coexist \u2014\nbut in practice clients should pick one.", "properties": {"published_course_id": {"description": "Obfuscated id of an existing PublishedCourse in the caller's organization.", "maxLength": 64, "title": "Published Course Id", "type": "string"}, "is_visible": {"default": true, "description": "True for allowlist (show despite default-hidden); False for blocklist.", "title": "Is Visible", "type": "boolean"}}, "required": ["published_course_id"], "title": "VisibilityOverrideAddRequest", "type": "object"}, "VisibilityOverrideAddRequestEnvelope": {"description": "JSON:API envelope for POST visibility-overrides.\n\nThe ``data`` field is always an array \u2014 single-add is a list of one. Cap\nenforcement (<= DEFAULT_BATCH_MAX_ITEMS items) lives in the endpoint, NOT\nhere \u2014 matches the create/update/delete contract (400 via ValidationError\ninstead of Pydantic 422).", "properties": {"data": {"description": "List of visibility-override items to add.", "items": {"$ref": "#/components/schemas/_VisibilityOverrideAddData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "VisibilityOverrideAddRequestEnvelope", "type": "object"}, "_VisibilityOverrideAddData": {"description": "JSON:API data wrapper for a single POST visibility-override item.", "properties": {"type": {"const": "visibility-overrides", "description": "Must be \"visibility-overrides\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/VisibilityOverrideAddRequest"}}, "required": ["type", "attributes"], "title": "_VisibilityOverrideAddData", "type": "object"}, "VisibilityOverrideRemoveRequest": {"description": "Request attributes for an item in DELETE visibility-overrides.\n\nMirrors :class:`VisibilityOverrideAddRequest` \u2014 DELETE identifies a row by\nthe same ``(published_course_id, is_visible)`` tuple that POST uses, so\nclients that hold a list of resources can round-trip the same attributes\nthrough DELETE.", "properties": {"published_course_id": {"description": "Obfuscated id of the PublishedCourse whose override should be removed.", "maxLength": 64, "title": "Published Course Id", "type": "string"}, "is_visible": {"default": true, "description": "True to remove an allowlist override; False to remove a blocklist override. Defaults to True to mirror POST; clients managing both allow and block rows for the same course must be explicit. See ``api_v2/docs/groups.md`` section: is_visible default on DELETE.", "title": "Is Visible", "type": "boolean"}}, "required": ["published_course_id"], "title": "VisibilityOverrideRemoveRequest", "type": "object"}, "VisibilityOverrideRemoveRequestEnvelope": {"description": "JSON:API envelope for DELETE visibility-overrides.", "properties": {"data": {"description": "List of visibility-override items to remove.", "items": {"$ref": "#/components/schemas/_VisibilityOverrideRemoveData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "VisibilityOverrideRemoveRequestEnvelope", "type": "object"}, "_VisibilityOverrideRemoveData": {"description": "JSON:API data wrapper for a single DELETE visibility-override item.", "properties": {"type": {"const": "visibility-overrides", "description": "Must be \"visibility-overrides\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/VisibilityOverrideRemoveRequest"}}, "required": ["type", "attributes"], "title": "_VisibilityOverrideRemoveData", "type": "object"}, "BatchResultEnvelope_MembershipResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_MembershipResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[MembershipResource]", "type": "object"}, "BatchSucceededItem_MembershipResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/MembershipResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[MembershipResource]", "type": "object"}, "MembershipAttributes": {"description": "Attributes of a membership resource (returned for successful add rows).\n\n``student_id`` is the obfuscated id of the student User row, useful for\nfollow-up calls against /v2/students/. ``is_manager`` is False for\nnewly-created rows (this endpoint never sets ``group_manager_assigned_at``);\non idempotent re-adds it reflects the existing row's manager flag, so a\nre-add of a previously-promoted manager surfaces ``is_manager=true`` \u2014\npromotion itself is a separate operation outside this endpoint's surface.", "properties": {"student_id": {"description": "Obfuscated id of the student User row.", "title": "Student Id", "type": "string"}, "is_manager": {"description": "True if the student is a manager for the group.", "title": "Is Manager", "type": "boolean"}, "created_at": {"description": "Timestamp when the membership was created.", "format": "date-time", "title": "Created At", "type": "string"}}, "required": ["student_id", "is_manager", "created_at"], "title": "MembershipAttributes", "type": "object"}, "MembershipResource": {"description": "JSON:API resource object for a StudentGroupMembership row.\n\nReturned for every successful ``add`` row \u2014 newly created and idempotent\nre-adds alike (the existing row is returned in the same shape).", "properties": {"type": {"const": "memberships", "default": "memberships", "description": "Always \"memberships\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated id of the StudentGroupMembership row.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/MembershipAttributes"}}, "required": ["id", "attributes"], "title": "MembershipResource", "type": "object"}, "MembershipAddRequestEnvelope": {"description": "JSON:API envelope for POST /v2/groups/{id}/relationships/students/.\n\nThe ``data`` field is always an array \u2014 single-add is a list of one. Cap\nenforcement (<= DEFAULT_BATCH_MAX_ITEMS items) lives in the endpoint, NOT\nhere \u2014 matches the create/update/delete contract (400 via ValidationError\ninstead of Pydantic 422).", "properties": {"data": {"description": "List of students to add to the group.", "items": {"$ref": "#/components/schemas/_MembershipStudentRefData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "MembershipAddRequestEnvelope", "type": "object"}, "_MembershipStudentRefData": {"additionalProperties": false, "description": "JSON:API resource identifier for a student in the memberships relationship.\n\n``extra=\"forbid\"`` rejects unknown keys (e.g. a stray ``attributes`` block\na client copies from POST ``/v2/students/``) so a malformed payload\nsurfaces as 422 at the schema layer rather than a silent no-op.", "properties": {"type": {"const": "students", "description": "Must be \"students\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated id of the student User row.", "maxLength": 64, "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "_MembershipStudentRefData", "type": "object"}, "MembershipRemoveRequestEnvelope": {"description": "JSON:API envelope for DELETE /v2/groups/{id}/relationships/students/.\n\nMirrors :class:`MembershipAddRequestEnvelope`: ``data`` is always an\narray (single-remove is a list of one), and cap enforcement\n(<= DEFAULT_BATCH_MAX_ITEMS items) lives in the endpoint via\n``_validate_batch_envelope`` \u2014 400 via ValidationError, not Pydantic 422.", "properties": {"data": {"description": "List of students to remove from the group.", "items": {"$ref": "#/components/schemas/_MembershipStudentRefData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "MembershipRemoveRequestEnvelope", "type": "object"}, "BatchResultEnvelope_EnrollmentWriteResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_EnrollmentWriteResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[EnrollmentWriteResource]", "type": "object"}, "BatchSucceededItem_EnrollmentWriteResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/EnrollmentWriteResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[EnrollmentWriteResource]", "type": "object"}, "EnrollmentWriteAttributes": {"description": "Attributes of the bulk-enroll (write) response resource.\n\n``channel`` is intentionally absent from this write shape \u2014 it's an internal\nsource-of-enrollment dimension that Pulse doesn't consume.", "properties": {"enrolled_at": {"description": "Timestamp when the student was enrolled.", "format": "date-time", "title": "Enrolled At", "type": "string"}, "expires_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when access expires, or null for no expiry.", "title": "Expires At"}, "due_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp the course is due, or null when no due date.", "title": "Due At"}, "active": {"description": "Whether the enrollment is currently active.", "title": "Active", "type": "boolean"}}, "required": ["enrolled_at", "active"], "title": "EnrollmentWriteAttributes", "type": "object"}, "EnrollmentWriteResource": {"description": "JSON:API resource object for an enrollment.", "properties": {"type": {"default": "enrollments", "description": "Always \"enrollments\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque enrollment ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/EnrollmentWriteAttributes"}, "relationships": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Related student and published-course links.", "title": "Relationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self link.", "title": "Links"}}, "required": ["id", "attributes"], "title": "EnrollmentWriteResource", "type": "object"}, "EnrollmentBatchCreateRequest": {"description": "Hybrid JSON:API envelope for POST /v2/enrollments/.\n\nTop-level ``published_course_id`` and ``expires_at`` are shared across every\nrow in ``data``. Per-row repetition is omitted by design (documented Pulse\nepic exception).\n\nCap enforcement and ``expires_at`` future-validation live in the endpoint \u2014\nnot here \u2014 so they surface as HTTP 400 via ``ValidationError``, matching the\nticket AC (\"past timestamps return 400, request-level validation\").", "properties": {"published_course_id": {"description": "Obfuscated id of the target PublishedCourse.", "title": "Published Course Id", "type": "string"}, "expires_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO 8601 timestamp when the enrollments expire. Optional. Must be in the future when provided.", "title": "Expires At"}, "data": {"description": "List of enrollment items to create.", "items": {"$ref": "#/components/schemas/_EnrollmentCreateData"}, "title": "Data", "type": "array"}}, "required": ["published_course_id", "data"], "title": "EnrollmentBatchCreateRequest", "type": "object"}, "EnrollmentCreateRequest": {"description": "Per-row attributes for bulk-enroll.", "properties": {"email": {"description": "Student's email address. Normalized to lowercase server-side.", "format": "email", "title": "Email", "type": "string"}}, "required": ["email"], "title": "EnrollmentCreateRequest", "type": "object"}, "_EnrollmentCreateData": {"description": "JSON:API data wrapper for one row in a bulk-enroll request.", "properties": {"type": {"const": "enrollments", "description": "Must be \"enrollments\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/EnrollmentCreateRequest"}}, "required": ["type", "attributes"], "title": "_EnrollmentCreateData", "type": "object"}, "EnrollmentUpdateAttributes": {"description": "Updatable attributes for a bulk enrollment update. Omitted fields are left unchanged.", "properties": {"expires_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when access expires; null clears the expiry.", "title": "Expires At"}, "due_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp the course is due; null clears the due date.", "title": "Due At"}, "active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "false deactivates the enrollment, true reactivates it (null is invalid; omit to leave unchanged).", "title": "Active"}}, "title": "EnrollmentUpdateAttributes", "type": "object"}, "EnrollmentUpdateRequestEnvelope": {"description": "JSON:API envelope for the bulk PATCH /v2/enrollments/ update.", "properties": {"data": {"description": "List of enrollment items to update.", "items": {"$ref": "#/components/schemas/_EnrollmentUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "EnrollmentUpdateRequestEnvelope", "type": "object"}, "_EnrollmentUpdateData": {"description": "JSON:API data object for one row of a bulk enrollment update.", "properties": {"type": {"const": "enrollments", "description": "Must be \"enrollments\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque enrollment ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/EnrollmentUpdateAttributes"}}, "required": ["type", "id", "attributes"], "title": "_EnrollmentUpdateData", "type": "object"}, "EnrollmentFilters": {"description": "Query parameters for GET /v2/enrollments/.", "properties": {"filter[student.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only enrollments for the student with this opaque id. Unknown ids match nothing.", "title": "Filter[Student.Id]"}, "filter[student.email]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only enrollments for the student with this email (case-insensitive). Unknown emails match nothing.", "title": "Filter[Student.Email]"}, "filter[course.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only enrollments in the course with this opaque id. Unknown or malformed ids match nothing.", "title": "Filter[Course.Id]"}, "filter[enrolled_gte]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments created on or after this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Enrolled Gte]"}, "filter[enrolled_lte]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments created on or before this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Enrolled Lte]"}, "filter[completed_gte]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments completed on or after this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Completed Gte]"}, "filter[completed_lte]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Return only enrollments completed on or before this ISO 8601 timestamp. Must include a timezone offset (e.g. Z or +00:00); naive datetimes are rejected with 422.", "title": "Filter[Completed Lte]"}, "filter[progress_status]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated progress states to include: completed, in_progress, not_started.", "title": "Filter[Progress Status]"}, "filter[domains]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; returns only enrollments whose published course is in one of those domains. Unknown names match nothing, never error.", "title": "Filter[Domains]"}, "filter[active]": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Return only active (true) or inactive (false) enrollments. Active means the enrollment is active and not expired; inactive means deactivated or past its expiry. Omit to return both.", "title": "Filter[Active]"}, "include": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated related resources to include in the response. Supported: purchase, student, certificate.", "title": "Include"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "EnrollmentFilters", "type": "object"}, "CertificateIdentifier": {"description": "JSON:API resource identifier for a certificate.", "properties": {"type": {"default": "certificates", "description": "Always \"certificates\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque certificate ID.", "title": "Id", "type": "string"}}, "required": ["id"], "title": "CertificateIdentifier", "type": "object"}, "CertificateRelationship": {"description": "To-one linkage to a certificate; ``data`` is null when there is no certificate.", "properties": {"data": {"anyOf": [{"$ref": "#/components/schemas/CertificateIdentifier"}, {"type": "null"}]}}, "title": "CertificateRelationship", "type": "object"}, "EnrollmentAttributes": {"description": "Attributes of an enrollment resource in the enrollments list response.", "properties": {"enrolled_at": {"description": "Timestamp when the student was enrolled.", "format": "date-time", "title": "Enrolled At", "type": "string"}, "expires_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when access expires, or null for no expiry.", "title": "Expires At"}, "due_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp the course is due, or null when no due date.", "title": "Due At"}, "active": {"description": "Whether this enrollment row is currently active.", "title": "Active", "type": "boolean"}, "channel": {"description": "The channel the enrollment was created through, e.g. api, platform, import.", "title": "Channel", "type": "string"}, "source": {"description": "Derived origin: purchased, self-enrolled, or assigned.", "title": "Source", "type": "string"}, "progress_status": {"description": "Progress state: completed, in_progress, or not_started.", "title": "Progress Status", "type": "string"}, "completed_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp the course was completed, or null.", "title": "Completed At"}, "score": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Student's stored score for the course, or null.", "title": "Score"}, "max_score": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Maximum possible stored score for the course, or null.", "title": "Max Score"}, "success_status": {"description": "Derived success state: passed, failed, pending (graded content without an outcome yet), or not_applicable (no graded content).", "title": "Success Status", "type": "string"}, "domain_name": {"description": "Hostname of the published course's domain.", "title": "Domain Name", "type": "string"}, "has_certificate": {"description": "True when a certificate is linked to this enrollment row, regardless of its progress state. Unrelated to whether the course is configured to issue certificates.", "title": "Has Certificate", "type": "boolean"}, "rating": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Student's course rating, or null when unrated.", "title": "Rating"}, "rating_scale": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Scale the rating was given on (5 or 10), or null when unrated.", "title": "Rating Scale"}, "feedback": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Free-text rating feedback, or null when none was left.", "title": "Feedback"}}, "required": ["enrolled_at", "active", "channel", "source", "progress_status", "success_status", "domain_name", "has_certificate"], "title": "EnrollmentAttributes", "type": "object"}, "EnrollmentListResponse": {"description": "Enrollments list envelope; ``included`` carries the resources requested via ?include=.", "properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/EnrollmentResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"anyOf": [{"$ref": "#/components/schemas/PurchaseResource"}, {"$ref": "#/components/schemas/StudentResource"}, {"$ref": "#/components/schemas/CertificateResource"}]}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when not requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "EnrollmentListResponse", "type": "object"}, "EnrollmentRelationships": {"description": "Relationships of an enrollment resource.", "properties": {"student": {"$ref": "#/components/schemas/StudentRelationship"}, "published-course": {"$ref": "#/components/schemas/PublishedCourseRelationship"}, "purchase": {"$ref": "#/components/schemas/PurchaseRelationship"}, "certificate": {"$ref": "#/components/schemas/CertificateRelationship"}}, "required": ["student", "published-course", "purchase", "certificate"], "title": "EnrollmentRelationships", "type": "object"}, "EnrollmentResource": {"description": "JSON:API resource object for an enrollment row.", "properties": {"type": {"default": "enrollments", "description": "Always \"enrollments\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque enrollment ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/EnrollmentAttributes"}, "relationships": {"$ref": "#/components/schemas/EnrollmentRelationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self link.", "title": "Links"}}, "required": ["id", "attributes", "relationships"], "title": "EnrollmentResource", "type": "object"}, "Money": {"description": "A monetary amount in a specific currency.", "properties": {"amount": {"description": "Decimal amount as a string, e.g. \"25.50\".", "title": "Amount", "type": "string"}, "currency_code": {"description": "ISO 4217 currency code, e.g. USD.", "title": "Currency Code", "type": "string"}}, "required": ["amount", "currency_code"], "title": "Money", "type": "object"}, "PublishedCourseIdentifier": {"description": "JSON:API resource identifier for a published course.", "properties": {"type": {"default": "published-courses", "description": "Always \"published-courses\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque published-course ID.", "title": "Id", "type": "string"}}, "required": ["id"], "title": "PublishedCourseIdentifier", "type": "object"}, "PublishedCourseRelationship": {"description": "To-one linkage to a published course.", "properties": {"data": {"$ref": "#/components/schemas/PublishedCourseIdentifier"}}, "required": ["data"], "title": "PublishedCourseRelationship", "type": "object"}, "PurchaseAttributes": {"description": "Attributes of a purchase resource included via ?include=purchase.", "properties": {"order_id": {"description": "Human-facing order number of the purchase.", "title": "Order Id", "type": "string"}, "offer_price": {"anyOf": [{"$ref": "#/components/schemas/Money"}, {"type": "null"}], "description": "Offer price at purchase time, or null when not recorded."}, "purchase_price": {"$ref": "#/components/schemas/Money", "description": "Amount the student paid."}, "quantity": {"description": "Number of seats purchased.", "title": "Quantity", "type": "integer"}, "purchase_state": {"description": "Purchase state, e.g. CREATED, SUCCESS, REFUNDED.", "title": "Purchase State", "type": "string"}, "payment_processor": {"description": "Payment processor, e.g. NONE, STRIPE, PAYPAL.", "title": "Payment Processor", "type": "string"}, "purchased_at": {"description": "Timestamp the purchase was made.", "format": "date-time", "title": "Purchased At", "type": "string"}, "tax_price_cents": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Tax in cents, or null when not recorded.", "title": "Tax Price Cents"}}, "required": ["order_id", "purchase_price", "quantity", "purchase_state", "payment_processor", "purchased_at"], "title": "PurchaseAttributes", "type": "object"}, "PurchaseIdentifier": {"description": "JSON:API resource identifier for a purchase.", "properties": {"type": {"default": "purchases", "description": "Always \"purchases\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque purchase ID.", "title": "Id", "type": "string"}}, "required": ["id"], "title": "PurchaseIdentifier", "type": "object"}, "PurchaseRelationship": {"description": "To-one linkage to a purchase; ``data`` is null when there is no purchase.", "properties": {"data": {"anyOf": [{"$ref": "#/components/schemas/PurchaseIdentifier"}, {"type": "null"}]}}, "title": "PurchaseRelationship", "type": "object"}, "PurchaseResource": {"description": "JSON:API resource object for a purchase included via ?include=purchase.", "properties": {"type": {"default": "purchases", "description": "Always \"purchases\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque purchase ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/PurchaseAttributes"}}, "required": ["id", "attributes"], "title": "PurchaseResource", "type": "object"}, "BatchResultEnvelope_EnrollmentCompletionResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_EnrollmentCompletionResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[EnrollmentCompletionResource]", "type": "object"}, "BatchSucceededItem_EnrollmentCompletionResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/EnrollmentCompletionResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[EnrollmentCompletionResource]", "type": "object"}, "EnrollmentCompletionResource": {"description": "JSON:API resource object for an enrollment completion update.", "properties": {"type": {"default": "enrollments", "description": "Always \"enrollments\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque enrollment ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/EnrollmentCompletionWriteAttributes"}}, "required": ["id", "attributes"], "title": "EnrollmentCompletionResource", "type": "object"}, "EnrollmentCompletionWriteAttributes": {"description": "Attributes of the completion write response resource.", "properties": {"completed_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the course was completed, or null.", "title": "Completed At"}, "success_status": {"description": "Derived success state: passed, failed, pending (graded content without an outcome yet), or not_applicable (no graded content).", "title": "Success Status", "type": "string"}}, "required": ["success_status"], "title": "EnrollmentCompletionWriteAttributes", "type": "object"}, "EnrollmentCompletionAttributes": {"description": "Updatable completion attributes for a bulk enrollment completion update. Omitted fields are left unchanged.", "properties": {"completed_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the course was completed. Null removes the completion.", "title": "Completed At"}, "success_status": {"anyOf": [{"enum": ["passed", "failed"], "type": "string"}, {"type": "null"}], "description": "Outcome of graded content: passed or failed. Null clears the status.", "title": "Success Status"}}, "title": "EnrollmentCompletionAttributes", "type": "object"}, "EnrollmentCompletionData": {"description": "JSON:API data object for one row of a bulk enrollment completion update.", "properties": {"type": {"const": "enrollments", "description": "Must be \"enrollments\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque enrollment ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/EnrollmentCompletionAttributes"}}, "required": ["type", "id", "attributes"], "title": "EnrollmentCompletionData", "type": "object"}, "EnrollmentCompletionRequestEnvelope": {"description": "JSON:API envelope for the bulk PATCH /v2/enrollments/completion update.", "properties": {"send_notifications": {"description": "Whether to send completion notification emails to students.", "title": "Send Notifications", "type": "boolean"}, "data": {"description": "List of enrollment completion items to update.", "items": {"$ref": "#/components/schemas/EnrollmentCompletionData"}, "title": "Data", "type": "array"}}, "required": ["send_notifications", "data"], "title": "EnrollmentCompletionRequestEnvelope", "type": "object"}, "EnrollmentDetailResponse": {"description": "Single enrollment resource response for GET /v2/enrollments/{id}/.", "properties": {"data": {"$ref": "#/components/schemas/EnrollmentResource"}, "included": {"anyOf": [{"items": {"anyOf": [{"$ref": "#/components/schemas/PurchaseResource"}, {"$ref": "#/components/schemas/StudentResource"}, {"$ref": "#/components/schemas/CertificateResource"}]}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when not requested.", "title": "Included"}}, "required": ["data"], "title": "EnrollmentDetailResponse", "type": "object"}, "QuizFilters": {"description": "Query parameters for GET /v2/quizzes/.\n\nPagination (``page[size]`` / ``page[cursor]``) is read directly from the\nrequest by ``CursorPaginator`` and is not declared here, mirroring the\n``LessonFilters`` / ``CourseFilters`` convention.", "properties": {"filter[name]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Quiz name. Case-insensitive exact match.", "title": "Filter[Name]"}, "filter[updated_since]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns quizzes updated at or after this time. Naive datetimes without an offset are rejected with 422.", "title": "Filter[Updated Since]"}}, "title": "QuizFilters", "type": "object"}, "PaginatedListResponse_QuizResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/QuizResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[QuizResource]", "type": "object"}, "QuizAttributes": {"description": "Attributes of a quiz resource object.\n\nFields mirror the ``quiz.models.Quiz`` columns exposed on the public API.\n``organization`` and internal primary keys are deliberately not exposed \u2014\n``id`` (obfuscated) and ``external_id`` (UUID5) are the only identifiers a\nclient sees.", "properties": {"name": {"description": "Quiz name.", "title": "Name", "type": "string"}, "description_html": {"default": "", "description": "HTML description shown on the quiz start screen.", "title": "Description Html", "type": "string"}, "passing_percentage_correct": {"description": "Percentage of questions a student must answer correctly to pass.", "title": "Passing Percentage Correct", "type": "integer"}, "max_attempts": {"description": "Maximum attempts allowed. 0 means unlimited.", "title": "Max Attempts", "type": "integer"}, "require_correct_response": {"description": "If true, students must answer each question correctly before proceeding.", "title": "Require Correct Response", "type": "boolean"}, "randomize_questions": {"description": "If true, questions are presented in a random order.", "title": "Randomize Questions", "type": "boolean"}, "limit_question_count": {"description": "Number of questions presented (selected at random). 0 means all questions.", "title": "Limit Question Count", "type": "integer"}, "randomize_answers": {"description": "If true, answer choices are presented in a random order.", "title": "Randomize Answers", "type": "boolean"}, "show_results_on_failure": {"description": "If true, students who fail can review their submitted answers and per-question status.", "title": "Show Results On Failure", "type": "boolean"}, "show_question_feedback": {"description": "If true, students receive per-question feedback on auto-graded questions.", "title": "Show Question Feedback", "type": "boolean"}, "time_limit_seconds": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Time limit in seconds, or null for unlimited time.", "title": "Time Limit Seconds"}, "skip_start_screen": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "If true, the start screen and description are hidden. May be null.", "title": "Skip Start Screen"}, "alignment": {"description": "Text alignment for the description and start screen.", "enum": ["left", "center", "right"], "title": "Alignment", "type": "string"}, "external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "created_at": {"description": "Timestamp when the quiz was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the quiz was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["name", "passing_percentage_correct", "max_attempts", "require_correct_response", "randomize_questions", "limit_question_count", "randomize_answers", "show_results_on_failure", "show_question_feedback", "alignment", "external_id", "created_at", "modified_at"], "title": "QuizAttributes", "type": "object"}, "QuizResource": {"description": "JSON:API resource object for a quiz.", "properties": {"type": {"const": "quizzes", "default": "quizzes", "description": "Always \"quizzes\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque quiz ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuizAttributes"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "QuizResource", "type": "object"}, "BatchResultEnvelope_QuizResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_QuizResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[QuizResource]", "type": "object"}, "BatchSucceededItem_QuizResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/QuizResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[QuizResource]", "type": "object"}, "QuizCreateRequest": {"additionalProperties": false, "description": "Request schema for creating a new quiz.\n\nOnly ``name`` is required. Every other field defaults to the same value as\nthe underlying ``Quiz`` model column, so ``model_dump()`` produces a dict\nthat can be splatted straight into ``Quiz.objects.create()`` without\noverriding any model default with an unintended value.\n\nNumeric and choice constraints are ported from ``quiz.models.Quiz`` and\n``api/v1/quizzes`` serializers: ``passing_percentage_correct`` is capped at\n100, ``max_attempts`` / ``limit_question_count`` are non-negative and capped\nat the int32 column limit, and ``time_limit_seconds`` is bounded by\n``QUIZ_MAX_TIME_LIMIT_SECONDS``.\n\n``extra=\"forbid\"`` rejects unknown attribute keys with a 422 at the schema\nlayer (matching ``QuizUpdateRequest`` and the ``api_v2.groups.schemas``\nprecedent) rather than silently dropping them.", "properties": {"name": {"description": "Quiz name.", "maxLength": 500, "title": "Name", "type": "string"}, "description_html": {"default": "", "description": "Optional HTML description shown on the quiz start screen.", "title": "Description Html", "type": "string"}, "passing_percentage_correct": {"default": 0, "description": "Percentage of questions a student must answer correctly to pass (0-100).", "maximum": 100, "minimum": 0, "title": "Passing Percentage Correct", "type": "integer"}, "max_attempts": {"default": 0, "description": "Maximum attempts allowed. 0 means unlimited.", "maximum": 2147483647, "minimum": 0, "title": "Max Attempts", "type": "integer"}, "require_correct_response": {"default": false, "description": "If true, students must answer each question correctly before proceeding.", "title": "Require Correct Response", "type": "boolean"}, "randomize_questions": {"default": false, "description": "If true, questions are presented in a random order.", "title": "Randomize Questions", "type": "boolean"}, "limit_question_count": {"default": 0, "description": "Number of questions presented (selected at random). 0 means all questions.", "maximum": 2147483647, "minimum": 0, "title": "Limit Question Count", "type": "integer"}, "randomize_answers": {"default": false, "description": "If true, answer choices are presented in a random order.", "title": "Randomize Answers", "type": "boolean"}, "show_results_on_failure": {"default": false, "description": "If true, students who fail can review their submitted answers and per-question status.", "title": "Show Results On Failure", "type": "boolean"}, "show_question_feedback": {"default": false, "description": "If true, students receive per-question feedback on auto-graded questions.", "title": "Show Question Feedback", "type": "boolean"}, "time_limit_seconds": {"anyOf": [{"maximum": 3600000, "minimum": 0, "type": "integer"}, {"type": "null"}], "description": "Time limit in seconds, or null for unlimited time.", "title": "Time Limit Seconds"}, "skip_start_screen": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "If true, the start screen and description are hidden. May be null.", "title": "Skip Start Screen"}, "alignment": {"default": "center", "description": "Text alignment for the description and start screen (left, center, or right).", "enum": ["left", "center", "right"], "title": "Alignment", "type": "string"}}, "required": ["name"], "title": "QuizCreateRequest", "type": "object"}, "QuizCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/quizzes/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<=100 items) lives in the endpoint, NOT here \u2014 that gives\nus HTTP 400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of quiz items to create.", "items": {"$ref": "#/components/schemas/_QuizCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuizCreateRequestEnvelope", "type": "object"}, "_QuizCreateData": {"description": "JSON:API data wrapper for quiz creation.", "properties": {"type": {"const": "quizzes", "description": "Must be \"quizzes\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuizCreateRequest"}}, "required": ["type", "attributes"], "title": "_QuizCreateData", "type": "object"}, "QuizBatchUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/quizzes/ batch updates.\n\nThe ``data`` field is always an array. Cap enforcement (<=100 items) lives\nin the endpoint so violations produce HTTP 400 rather than Pydantic 422.", "properties": {"data": {"description": "List of quiz update items.", "items": {"$ref": "#/components/schemas/_QuizBatchUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuizBatchUpdateRequestEnvelope", "type": "object"}, "QuizUpdateRequest": {"additionalProperties": false, "description": "Request schema for partial updates of a quiz.\n\nNon-nullable fields are typed as non-Optional so Pydantic rejects explicit\n``null`` at the type level. ``time_limit_seconds`` and ``skip_start_screen``\nare genuinely nullable columns, so they stay ``| None`` and accept an\nexplicit ``null`` to clear/blank them. PATCH semantics are preserved by\nPython-level sentinel defaults combined with ``model_fields_set``: an\nomitted field keeps its default but is absent from ``model_fields_set``, so\nthe endpoint forwards only explicitly-provided fields via\n``model_dump(exclude_unset=True)``. Sentinel defaults are stripped from the\nemitted JSON Schema so SDK generators do not advertise them.\n\n``extra=\"forbid\"`` rejects unknown attribute keys with a 422 at the schema\nlayer (mirrors ``api_v2.groups.schemas`` precedent) rather than silently\ndropping them. The service also guards with ``_ALLOWED_UPDATE_FIELDS`` as\ndefense-in-depth for any non-HTTP caller.", "properties": {"name": {"description": "New quiz name. Must be non-empty when provided.", "maxLength": 500, "title": "Name", "type": "string"}, "description_html": {"description": "New HTML description shown on the quiz start screen. Empty string \"\" is accepted and clears the description.", "title": "Description Html", "type": "string"}, "passing_percentage_correct": {"description": "New passing percentage (0-100).", "maximum": 100, "minimum": 0, "title": "Passing Percentage Correct", "type": "integer"}, "max_attempts": {"description": "New maximum attempts. 0 means unlimited.", "maximum": 2147483647, "minimum": 0, "title": "Max Attempts", "type": "integer"}, "require_correct_response": {"description": "If true, students must answer each question correctly before proceeding.", "title": "Require Correct Response", "type": "boolean"}, "randomize_questions": {"description": "If true, questions are presented in a random order.", "title": "Randomize Questions", "type": "boolean"}, "limit_question_count": {"description": "Number of questions presented (selected at random). 0 means all questions.", "maximum": 2147483647, "minimum": 0, "title": "Limit Question Count", "type": "integer"}, "randomize_answers": {"description": "If true, answer choices are presented in a random order.", "title": "Randomize Answers", "type": "boolean"}, "show_results_on_failure": {"description": "If true, students who fail can review their submitted answers and per-question status.", "title": "Show Results On Failure", "type": "boolean"}, "show_question_feedback": {"description": "If true, students receive per-question feedback on auto-graded questions.", "title": "Show Question Feedback", "type": "boolean"}, "time_limit_seconds": {"anyOf": [{"maximum": 3600000, "minimum": 0, "type": "integer"}, {"type": "null"}], "description": "New time limit in seconds, or null for unlimited time.", "title": "Time Limit Seconds"}, "skip_start_screen": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "If true, the start screen and description are hidden. May be null.", "title": "Skip Start Screen"}, "alignment": {"description": "Text alignment for the description and start screen (left, center, or right).", "enum": ["left", "center", "right"], "title": "Alignment", "type": "string"}}, "title": "QuizUpdateRequest", "type": "object"}, "_QuizBatchUpdateData": {"description": "JSON:API data wrapper for a single item in a batch quiz update.", "properties": {"type": {"const": "quizzes", "description": "Must be \"quizzes\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the quiz to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuizUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_QuizBatchUpdateData", "type": "object"}, "QuizDeleteRequestEnvelope": {"description": "JSON:API envelope for DELETE /v2/quizzes/ batch delete.\n\nThe ``data`` field is always an array \u2014 single-delete is a list of one.\nCap enforcement (<=100 items) lives in the endpoint so violations produce\nHTTP 400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of quiz items to delete.", "items": {"$ref": "#/components/schemas/_QuizDeleteData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuizDeleteRequestEnvelope", "type": "object"}, "_QuizDeleteData": {"description": "JSON:API resource-identifier object for a single quiz delete item.\n\nDelete items carry only ``type`` and ``id`` \u2014 no ``attributes`` \u2014 matching\nthe resource-identifier object shape from JSON:API \u00a71.0. This mirrors the\nresource-identifier SHAPE (``type`` + ``id``) of\n``api_v2.groups.schemas._GroupDeleteData``, not its field-level validation:\nthat class's ``id`` ``max_length`` cap and blank-id validator are\ndeliberately NOT ported here (a separate hardening decision).", "properties": {"type": {"const": "quizzes", "description": "Must be \"quizzes\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the quiz to delete.", "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "_QuizDeleteData", "type": "object"}, "QuizDetailResponse": {"description": "Single-resource envelope for GET /v2/quizzes/{id}/ responses.", "properties": {"data": {"$ref": "#/components/schemas/QuizResource"}}, "required": ["data"], "title": "QuizDetailResponse", "type": "object"}, "QuestionBankAssignmentAttributes": {"description": "Attributes of a question-bank-assignment resource object.\n\nFields mirror the writable/observable ``quiz.models.QuestionBankAssignment``\ncolumns. ``question_bank_id`` (obfuscated) identifies the bound bank; the\nassignment's own obfuscated PK is the resource ``id``. No ``external_id`` \u2014\nthe model is not an ``ExternallyAuditableModel``.", "properties": {"question_bank_id": {"description": "Obfuscated ID of the bound question bank.", "title": "Question Bank Id", "type": "string"}, "order": {"description": "Ordering value of this bank within the quiz.", "title": "Order", "type": "integer"}, "randomize_questions": {"description": "If true, the bank's questions are presented in a random order.", "title": "Randomize Questions", "type": "boolean"}, "limit_question_count": {"description": "Number of questions drawn from this bank. 0 means all questions.", "title": "Limit Question Count", "type": "integer"}, "created_at": {"description": "Timestamp when the assignment was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the assignment was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["question_bank_id", "order", "randomize_questions", "limit_question_count", "created_at", "modified_at"], "title": "QuestionBankAssignmentAttributes", "type": "object"}, "QuestionBankAssignmentListResponse": {"description": "Response envelope for GET /v2/quizzes/{quiz_id}/question-banks/.\n\nA plain, non-paginated ``{\"data\": [...]}`` list: a quiz's bank assignments\nare a small bounded set, so \u2014 unlike the quizzes/question-banks/questions\nread lists \u2014 this endpoint does NOT use ``CursorPaginator`` and carries no\n``meta`` / ``links`` / ``next_cursor`` pagination fields. Ordering follows\nthe model's ``Meta.ordering`` (``order``, then ``created_at``).", "properties": {"data": {"description": "The quiz's bank assignments, ordered.", "items": {"$ref": "#/components/schemas/QuestionBankAssignmentResource"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankAssignmentListResponse", "type": "object"}, "QuestionBankAssignmentResource": {"description": "JSON:API resource object for a quiz\u2194question-bank assignment.", "properties": {"type": {"const": "question-bank-assignments", "default": "question-bank-assignments", "description": "Always \"question-bank-assignments\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque assignment ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionBankAssignmentAttributes"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "QuestionBankAssignmentResource", "type": "object"}, "BatchResultEnvelope_QuestionBankAssignmentResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_QuestionBankAssignmentResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[QuestionBankAssignmentResource]", "type": "object"}, "BatchSucceededItem_QuestionBankAssignmentResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/QuestionBankAssignmentResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[QuestionBankAssignmentResource]", "type": "object"}, "QuestionBankAssignmentBindRequest": {"additionalProperties": false, "description": "Request schema for binding a question bank to a quiz.\n\n``question_bank_id`` is required (the natural key alongside the path\n``quiz_id``). ``order`` is optional: when omitted (``None``) on a first bind\nthe service derives ``Max(order) + 10`` within the quiz's assignments\n(mirrors the question-order derivation). ``randomize_questions`` /\n``limit_question_count`` default to the model column defaults on a first bind.\n\nBinding is an idempotent partial update: re-binding an already-bound bank\nwrites only the fields the caller actually supplies, so any writable field\nOMITTED on the re-bind is PRESERVED at its stored value (it is NOT reset to a\nschema default, and an omitted ``order`` is NOT re-derived).\n\n``extra=\"forbid\"`` rejects unknown attribute keys with a 422 (mirrors\n``QuestionBankAssignmentUpdateRequest`` and the other api_v2 write schemas)\nso a typo'd field is a hard error rather than a silently-dropped no-op.", "properties": {"question_bank_id": {"description": "Obfuscated ID of the question bank to bind.", "title": "Question Bank Id", "type": "string"}, "order": {"anyOf": [{"maximum": 2147483647, "minimum": 0, "type": "integer"}, {"type": "null"}], "description": "Ordering value within the quiz. Omit to append after the current max order.", "title": "Order"}, "randomize_questions": {"description": "If true, the bank's questions are presented in a random order.", "title": "Randomize Questions", "type": "boolean"}, "limit_question_count": {"description": "Number of questions drawn from this bank. 0 means all questions.", "maximum": 2147483647, "minimum": 0, "title": "Limit Question Count", "type": "integer"}}, "required": ["question_bank_id"], "title": "QuestionBankAssignmentBindRequest", "type": "object"}, "QuestionBankAssignmentBindRequestEnvelope": {"description": "JSON:API envelope for POST /v2/quizzes/{quiz_id}/question-banks/.\n\nThe ``data`` field is always an array \u2014 single-bind is a list of one. Cap\nenforcement (<=100 items) lives in the endpoint so violations produce HTTP\n400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of bind items.", "items": {"$ref": "#/components/schemas/_QuestionBankAssignmentBindData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankAssignmentBindRequestEnvelope", "type": "object"}, "_QuestionBankAssignmentBindData": {"description": "JSON:API data wrapper for a single bind item.", "properties": {"type": {"const": "question-bank-assignments", "description": "Must be \"question-bank-assignments\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionBankAssignmentBindRequest"}}, "required": ["type", "attributes"], "title": "_QuestionBankAssignmentBindData", "type": "object"}, "QuestionBankAssignmentBatchUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/quizzes/{quiz_id}/question-banks/ batch updates.\n\nThe ``data`` field is always an array. Cap enforcement (<=100 items) lives\nin the endpoint so violations produce HTTP 400 rather than Pydantic 422.", "properties": {"data": {"description": "List of assignment update items.", "items": {"$ref": "#/components/schemas/_QuestionBankAssignmentBatchUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankAssignmentBatchUpdateRequestEnvelope", "type": "object"}, "QuestionBankAssignmentUpdateRequest": {"additionalProperties": false, "description": "Request schema for partial updates of a quiz\u2194question-bank assignment.\n\n``question_bank_id`` is required \u2014 it is the natural key (with the path\n``quiz_id``) identifying WHICH assignment to update, not a writable field.\n``order`` / ``randomize_questions`` / ``limit_question_count`` are the\nwritable fields and use PATCH semantics: a Python-level sentinel default\ncombined with ``model_fields_set`` means an omitted field stays out of\n``model_dump(exclude_unset=True)``. The endpoint pops ``question_bank_id``\nbefore forwarding the remaining fields as ``update_fields``.\n\n``extra=\"forbid\"`` rejects unknown attribute keys with a 422 at the schema\nlayer (mirrors ``QuizUpdateRequest``); the service also allowlist-guards the\nwritable set as defense-in-depth for any non-HTTP caller.", "properties": {"question_bank_id": {"description": "Obfuscated ID of the assigned question bank to update.", "title": "Question Bank Id", "type": "string"}, "order": {"description": "New ordering value within the quiz.", "maximum": 2147483647, "minimum": 0, "title": "Order", "type": "integer"}, "randomize_questions": {"description": "If true, the bank's questions are presented in a random order.", "title": "Randomize Questions", "type": "boolean"}, "limit_question_count": {"description": "Number of questions drawn from this bank. 0 means all questions.", "maximum": 2147483647, "minimum": 0, "title": "Limit Question Count", "type": "integer"}}, "required": ["question_bank_id"], "title": "QuestionBankAssignmentUpdateRequest", "type": "object"}, "_QuestionBankAssignmentBatchUpdateData": {"description": "JSON:API data wrapper for a single assignment update item.", "properties": {"type": {"const": "question-bank-assignments", "description": "Must be \"question-bank-assignments\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionBankAssignmentUpdateRequest"}}, "required": ["type", "attributes"], "title": "_QuestionBankAssignmentBatchUpdateData", "type": "object"}, "QuestionBankAssignmentDeleteRequestEnvelope": {"description": "JSON:API envelope for DELETE /v2/quizzes/{quiz_id}/question-banks/ batch unbind.\n\nThe ``data`` field is always an array. Cap enforcement (<=100 items) lives\nin the endpoint so violations produce HTTP 400 rather than Pydantic 422.", "properties": {"data": {"description": "List of unbind items.", "items": {"$ref": "#/components/schemas/_QuestionBankAssignmentDeleteData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankAssignmentDeleteRequestEnvelope", "type": "object"}, "_QuestionBankAssignmentDeleteAttributes": {"description": "Attributes of an unbind item \u2014 the ``question_bank_id`` natural key only.", "properties": {"question_bank_id": {"description": "Obfuscated ID of the question bank to unbind.", "title": "Question Bank Id", "type": "string"}}, "required": ["question_bank_id"], "title": "_QuestionBankAssignmentDeleteAttributes", "type": "object"}, "_QuestionBankAssignmentDeleteData": {"description": "JSON:API data wrapper for a single unbind item.\n\n``question_bank_id`` lives under ``attributes`` (not as a top-level ``id``)\nso it stays symmetric with the bind/update items and so the per-item\nJSON:API source pointer resolves to ``/data/{i}/attributes/question_bank_id``\nvia the shared ``dedup_first_wins`` / ``pointer_for`` helpers.", "properties": {"type": {"const": "question-bank-assignments", "description": "Must be \"question-bank-assignments\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/_QuestionBankAssignmentDeleteAttributes"}}, "required": ["type", "attributes"], "title": "_QuestionBankAssignmentDeleteData", "type": "object"}, "QuestionBankFilters": {"description": "Query parameters for GET /v2/question-banks/.\n\nPagination (``page[size]`` / ``page[cursor]``) is read directly from the\nrequest by ``CursorPaginator`` and is not declared here, mirroring the\n``QuizFilters`` convention.", "properties": {"filter[name]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Question bank name. Case-insensitive exact match.", "title": "Filter[Name]"}, "filter[updated_since]": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "ISO-8601 timestamp with timezone offset (e.g. ``2024-01-01T00:00:00Z``); returns question banks updated at or after this time. Naive datetimes without an offset are rejected with 422.", "title": "Filter[Updated Since]"}}, "title": "QuestionBankFilters", "type": "object"}, "PaginatedListResponse_QuestionBankResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/QuestionBankResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[QuestionBankResource]", "type": "object"}, "QuestionBankAttributes": {"description": "Attributes of a question-bank resource object.\n\nFields mirror the ``quiz.models.QuestionBank`` columns exposed on the public\nAPI. ``organization`` and internal primary keys are deliberately not exposed\n\u2014 ``id`` (obfuscated) and ``external_id`` (UUID5) are the only identifiers a\nclient sees.", "properties": {"name": {"description": "Question bank name.", "title": "Name", "type": "string"}, "external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "created_at": {"description": "Timestamp when the question bank was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the question bank was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["name", "external_id", "created_at", "modified_at"], "title": "QuestionBankAttributes", "type": "object"}, "QuestionBankResource": {"description": "JSON:API resource object for a question bank.", "properties": {"type": {"const": "question-banks", "default": "question-banks", "description": "Always \"question-banks\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque question bank ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionBankAttributes"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "QuestionBankResource", "type": "object"}, "BatchResultEnvelope_QuestionBankResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_QuestionBankResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[QuestionBankResource]", "type": "object"}, "BatchSucceededItem_QuestionBankResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/QuestionBankResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[QuestionBankResource]", "type": "object"}, "QuestionBankCreateRequest": {"additionalProperties": false, "description": "Request schema for creating a new question bank.\n\n``name`` is the only field; it is required and must be non-empty. The\n``model_dump()`` produces a dict that can be splatted straight into\n``QuestionBank.objects.create()``.\n\n``extra=\"forbid\"`` rejects unknown attribute keys with a 422 at the schema\nlayer (matching ``QuizCreateRequest`` and ``QuestionBankUpdateRequest``)\nrather than silently dropping them.", "properties": {"name": {"description": "Question bank name.", "maxLength": 500, "title": "Name", "type": "string"}}, "required": ["name"], "title": "QuestionBankCreateRequest", "type": "object"}, "QuestionBankCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/question-banks/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<=100 items) lives in the endpoint, NOT here \u2014 that gives\nus HTTP 400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of question-bank items to create.", "items": {"$ref": "#/components/schemas/_QuestionBankCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankCreateRequestEnvelope", "type": "object"}, "_QuestionBankCreateData": {"description": "JSON:API data wrapper for question-bank creation.", "properties": {"type": {"const": "question-banks", "description": "Must be \"question-banks\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionBankCreateRequest"}}, "required": ["type", "attributes"], "title": "_QuestionBankCreateData", "type": "object"}, "QuestionBankBatchUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/question-banks/ batch updates.\n\nThe ``data`` field is always an array. Cap enforcement (<=100 items) lives\nin the endpoint so violations produce HTTP 400 rather than Pydantic 422.", "properties": {"data": {"description": "List of question-bank update items.", "items": {"$ref": "#/components/schemas/_QuestionBankBatchUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankBatchUpdateRequestEnvelope", "type": "object"}, "QuestionBankUpdateRequest": {"additionalProperties": false, "description": "Request schema for partial updates of a question bank.\n\n``name`` is the only writable field. PATCH semantics are preserved by a\nPython-level sentinel default combined with ``model_fields_set``: an omitted\nfield keeps its default but is absent from ``model_fields_set``, so the\nendpoint forwards only explicitly-provided fields via\n``model_dump(exclude_unset=True)``. The sentinel default is stripped from the\nemitted JSON Schema so SDK generators do not advertise it.\n\n``extra=\"forbid\"`` rejects unknown attribute keys with a 422 at the schema\nlayer (mirrors ``QuizUpdateRequest``). The service also guards with\n``_ALLOWED_QB_UPDATE_FIELDS`` as defense-in-depth for any non-HTTP caller.", "properties": {"name": {"description": "New question bank name. Must be non-empty when provided.", "maxLength": 500, "title": "Name", "type": "string"}}, "title": "QuestionBankUpdateRequest", "type": "object"}, "_QuestionBankBatchUpdateData": {"description": "JSON:API data wrapper for a single item in a batch question-bank update.", "properties": {"type": {"const": "question-banks", "description": "Must be \"question-banks\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the question bank to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionBankUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_QuestionBankBatchUpdateData", "type": "object"}, "QuestionBankDeleteRequestEnvelope": {"description": "JSON:API envelope for DELETE /v2/question-banks/ batch delete.\n\nThe ``data`` field is always an array \u2014 single-delete is a list of one.\nCap enforcement (<=100 items) lives in the endpoint so violations produce\nHTTP 400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of question-bank items to delete.", "items": {"$ref": "#/components/schemas/_QuestionBankDeleteData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBankDeleteRequestEnvelope", "type": "object"}, "_QuestionBankDeleteData": {"description": "JSON:API resource-identifier object for a single question-bank delete item.\n\nDelete items carry only ``type`` and ``id`` \u2014 no ``attributes`` \u2014 mirroring\nthe resource-identifier SHAPE (``type`` + ``id``) of ``_QuizDeleteData`` and\n``api_v2.groups.schemas._GroupDeleteData``, not the latter's field-level\nvalidation (its ``id`` ``max_length`` cap and blank-id validator are not\nported here).", "properties": {"type": {"const": "question-banks", "description": "Must be \"question-banks\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the question bank to delete.", "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "_QuestionBankDeleteData", "type": "object"}, "QuestionBankDetailResponse": {"description": "Single-resource envelope for GET /v2/question-banks/{id}/ responses.", "properties": {"data": {"$ref": "#/components/schemas/QuestionBankResource"}}, "required": ["data"], "title": "QuestionBankDetailResponse", "type": "object"}, "QuestionFilters": {"description": "Query parameters for GET /v2/questions/.\n\nPagination (``page[size]`` / ``page[cursor]``) is read directly from the\nrequest by ``CursorPaginator`` and is not declared here, mirroring the\n``QuizFilters`` convention.\n\nA malformed obfuscated ``quiz_id`` / ``question_bank_id`` yields an empty\nresult rather than a 422 \u2014 filters express intent, and an intent for a\nparent that cannot exist is \"no rows\" (mirrors ``LessonFilters.course_id``).", "properties": {"filter[quiz_id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent quiz ID. Exact match.", "title": "Filter[Quiz Id]"}, "filter[question_bank_id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent question bank ID. Exact match.", "title": "Filter[Question Bank Id]"}}, "title": "QuestionFilters", "type": "object"}, "AnswerObject": {"description": "A single answer choice embedded within a question resource.\n\nAnswers are not a standalone JSON:API resource \u2014 they exist only nested in\n``QuestionAttributes.answers``. Soft-deleted answers are excluded and the\nlist is ordered by ``order`` ascending (see ``_build_question_resource``).", "properties": {"id": {"description": "Opaque answer ID.", "title": "Id", "type": "string"}, "answer_text": {"description": "The answer choice text.", "title": "Answer Text", "type": "string"}, "correct": {"description": "Whether this choice is a correct answer.", "title": "Correct", "type": "boolean"}, "order": {"description": "Ordering value within the parent question.", "title": "Order", "type": "integer"}}, "required": ["id", "answer_text", "correct", "order"], "title": "AnswerObject", "type": "object"}, "PaginatedListResponse_QuestionResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/QuestionResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[QuestionResource]", "type": "object"}, "QuestionAttributes": {"description": "Attributes of a question resource object.\n\nFields mirror the ``quiz.models.Question`` columns exposed on the public API.\n``organization`` and internal primary keys are deliberately not exposed \u2014\n``id`` (obfuscated) and ``external_id`` (UUID5) are the only identifiers a\nclient sees.\n\nA question is homed under exactly one parent: ``quiz_id`` XOR\n``question_bank_id``. Only the parent that is actually set is populated; the\nother is ``None``. The single-resource GET (``exclude_none=True``) omits the\nunset id; the list GET does not set ``exclude_none`` (it would strip the null\npagination ``links`` keys), so the unset id serializes as ``null`` there.", "properties": {"question_html": {"description": "HTML body of the question prompt.", "title": "Question Html", "type": "string"}, "order": {"description": "Ordering value within the parent quiz or question bank.", "title": "Order", "type": "integer"}, "question_type": {"description": "Question type code. One of MULTIPLE_CHOICE, MULTIPLE_ANSWER, FILL_IN_THE_BLANK, FREEFORM, CONTENT_UPLOAD, LINEAR_SCALE.", "title": "Question Type", "type": "string"}, "case_sensitive": {"description": "If true, student responses must match the answer's case to be correct.", "title": "Case Sensitive", "type": "boolean"}, "requires_manual_grading": {"description": "If true, responses to this question must be graded by a dashboard user.", "title": "Requires Manual Grading", "type": "boolean"}, "is_graded": {"description": "Whether the question counts toward the quiz score.", "title": "Is Graded", "type": "boolean"}, "is_optional": {"description": "Whether the question may be skipped.", "title": "Is Optional", "type": "boolean"}, "correct_answer_feedback_html": {"default": "", "description": "HTML feedback shown when the student answers correctly.", "title": "Correct Answer Feedback Html", "type": "string"}, "incorrect_answer_feedback_html": {"default": "", "description": "HTML feedback shown when the student answers incorrectly.", "title": "Incorrect Answer Feedback Html", "type": "string"}, "answer_feedback_html": {"default": "", "description": "HTML feedback shown regardless of correctness.", "title": "Answer Feedback Html", "type": "string"}, "quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated ID of the parent quiz, when the question is homed under a quiz.", "title": "Quiz Id"}, "question_bank_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated ID of the parent question bank, when the question is homed under a bank.", "title": "Question Bank Id"}, "external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "created_at": {"description": "Timestamp when the question was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the question was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}, "answers": {"description": "The question's answer choices, ordered by ``order`` ascending. Excludes soft-deleted answers.", "items": {"$ref": "#/components/schemas/AnswerObject"}, "title": "Answers", "type": "array"}}, "required": ["question_html", "order", "question_type", "case_sensitive", "requires_manual_grading", "is_graded", "is_optional", "external_id", "created_at", "modified_at"], "title": "QuestionAttributes", "type": "object"}, "QuestionResource": {"description": "JSON:API resource object for a question.", "properties": {"type": {"const": "questions", "default": "questions", "description": "Always \"questions\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque question ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionAttributes"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "QuestionResource", "type": "object"}, "BatchResultEnvelope_QuestionResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_QuestionResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[QuestionResource]", "type": "object"}, "BatchSucceededItem_QuestionResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/QuestionResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[QuestionResource]", "type": "object"}, "AnswerCreateRequest": {"additionalProperties": false, "description": "A single answer choice supplied on question creation.\n\n``order`` is intentionally NOT accepted \u2014 v1 treats answer order as\nread-only and derives it from array position (idx*10) in the service.\n``correct`` is accepted for every type for a uniform wire shape, but the\nservice forces it ``True`` for FILL_IN_THE_BLANK regardless of the value\nsent (mirrors ``BaseQuestionSerializer.create``).", "properties": {"answer_text": {"description": "The answer choice text.", "maxLength": 1000, "title": "Answer Text", "type": "string"}, "correct": {"default": false, "description": "Whether this choice is a correct answer.", "title": "Correct", "type": "boolean"}}, "required": ["answer_text"], "title": "AnswerCreateRequest", "type": "object"}, "QuestionCreateRequest": {"additionalProperties": false, "description": "Request schema for creating a question with its answers.\n\nA question is homed under exactly one parent \u2014 ``quiz_id`` XOR\n``question_bank_id`` (both -> 422, neither -> 422). ``question_type`` is a\nLiteral of the API-enabled subset so CONTENT_UPLOAD / LINEAR_SCALE are\nrejected with 422. Per-type rules are ported EXACTLY from\n``quiz/serializers.py::BaseQuestionSerializer`` (the ``_validate_*``\nmethods); the dispatch mirrors ``BaseQuestionSerializer.validate``.\n\nNeither the question ``order`` nor per-answer ``order`` is accepted \u2014 the\nservice auto-assigns question order (``get_order_for_new_question`` logic)\nand answer order (idx*10 by array position).\n\n``is_graded``, ``is_optional``, and ``answer_feedback_html`` are NOT writable\non create \u2014 v1's create serializer does not expose them either, so they take\ntheir ``Question`` model defaults. ``extra=\"forbid\"`` (the module create-schema\nconvention) turns an attempt to set them into a 422 rather than silently\ndropping the value.", "properties": {"quiz_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent quiz ID. Provide exactly one of quiz_id / question_bank_id.", "title": "Quiz Id"}, "question_bank_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Obfuscated parent question bank ID. Provide exactly one of quiz_id / question_bank_id.", "title": "Question Bank Id"}, "question_html": {"description": "HTML body of the question prompt. Must be non-empty.", "title": "Question Html", "type": "string"}, "question_type": {"description": "Question type. One of MULTIPLE_CHOICE, MULTIPLE_ANSWER, FILL_IN_THE_BLANK, FREEFORM.", "enum": ["MULTIPLE_CHOICE", "MULTIPLE_ANSWER", "FILL_IN_THE_BLANK", "FREEFORM"], "title": "Question Type", "type": "string"}, "case_sensitive": {"default": false, "description": "If true, student responses must match the answer's case. Only valid for FILL_IN_THE_BLANK.", "title": "Case Sensitive", "type": "boolean"}, "requires_manual_grading": {"default": false, "description": "If true, responses must be graded by a dashboard user. Not allowed on MC/MA/FITB.", "title": "Requires Manual Grading", "type": "boolean"}, "correct_answer_feedback_html": {"default": "", "description": "HTML feedback shown when the student answers correctly. Not allowed for FREEFORM.", "title": "Correct Answer Feedback Html", "type": "string"}, "incorrect_answer_feedback_html": {"default": "", "description": "HTML feedback shown when the student answers incorrectly. Not allowed for FREEFORM.", "title": "Incorrect Answer Feedback Html", "type": "string"}, "answers": {"description": "The question's answer choices. Must be empty for FREEFORM; >= 1 for the other types.", "items": {"$ref": "#/components/schemas/AnswerCreateRequest"}, "title": "Answers", "type": "array"}}, "required": ["question_html", "question_type"], "title": "QuestionCreateRequest", "type": "object"}, "QuestionCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/questions/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<=100 items) lives in the endpoint, NOT here \u2014 that gives\nHTTP 400 via ``ValidationError`` instead of Pydantic's 422 (matches the\nquizzes/lessons create envelopes).", "properties": {"data": {"description": "List of question items to create.", "items": {"$ref": "#/components/schemas/_QuestionCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionCreateRequestEnvelope", "type": "object"}, "_QuestionCreateData": {"description": "JSON:API data wrapper for question creation.", "properties": {"type": {"const": "questions", "description": "Must be \"questions\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionCreateRequest"}}, "required": ["type", "attributes"], "title": "_QuestionCreateData", "type": "object"}, "QuestionBatchUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/questions/ batch updates.\n\nThe ``data`` field is always an array. Cap enforcement (<=100 items) lives in\nthe endpoint so violations produce HTTP 400 rather than Pydantic 422.", "properties": {"data": {"description": "List of question update items.", "items": {"$ref": "#/components/schemas/_QuestionBatchUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionBatchUpdateRequestEnvelope", "type": "object"}, "QuestionUpdateRequest": {"additionalProperties": false, "description": "Request schema for partial updates of a question \u2014 QUESTION FIELDS ONLY.\n\nAnswers are IMMUTABLE on update (matching v1, which makes answers read-only on\na question update), so no ``answers`` field is exposed. ``question_type``,\n``quiz_id`` / ``question_bank_id`` (the parent \u2014 a question cannot be moved\nbetween a quiz and a bank, mirroring lessons where ``course_id`` is not\neditable), and ``order`` are likewise read-only and simply absent from the\nschema; ``extra=\"forbid\"`` rejects any of them with a 422 at the schema layer.\n\nPATCH semantics are preserved by Python-level sentinel defaults combined with\n``model_fields_set``: an omitted field keeps its default but is absent from\n``model_fields_set``, so the endpoint forwards only explicitly-provided fields\nvia ``model_dump(exclude_unset=True)``. Sentinel defaults are stripped from the\nemitted JSON Schema so SDK generators do not advertise them.\n\nOnly ``question_html`` carries a non-empty rule (mirrors the create-side rule);\nthe three feedback fields deliberately accept empty string as the clear\naffordance. The cross-state per-type FLAG re-validation (case_sensitive /\nrequires_manual_grading / feedback rules against the STORED ``question_type``)\nlives in ``services.update_question``, not here \u2014 it depends on the stored type,\nwhich the schema cannot see.", "properties": {"question_html": {"description": "New HTML body of the question prompt. Must be non-empty when provided.", "title": "Question Html", "type": "string"}, "correct_answer_feedback_html": {"description": "New HTML feedback shown when the student answers correctly. Empty string clears it. Not allowed on FREEFORM questions (rejected against the stored type).", "title": "Correct Answer Feedback Html", "type": "string"}, "incorrect_answer_feedback_html": {"description": "New HTML feedback shown when the student answers incorrectly. Empty string clears it. Not allowed on FREEFORM questions (rejected against the stored type).", "title": "Incorrect Answer Feedback Html", "type": "string"}, "answer_feedback_html": {"description": "New HTML feedback shown regardless of correctness. Empty string clears it.", "title": "Answer Feedback Html", "type": "string"}, "case_sensitive": {"description": "If true, student responses must match the answer's case. Only valid on FILL_IN_THE_BLANK (rejected against the stored type for other types).", "title": "Case Sensitive", "type": "boolean"}, "requires_manual_grading": {"description": "If true, responses must be graded by a dashboard user. Not allowed on MULTIPLE_CHOICE / MULTIPLE_ANSWER / FILL_IN_THE_BLANK (rejected against the stored type).", "title": "Requires Manual Grading", "type": "boolean"}, "is_graded": {"description": "Whether the question counts toward the quiz score.", "title": "Is Graded", "type": "boolean"}, "is_optional": {"description": "Whether the question may be skipped.", "title": "Is Optional", "type": "boolean"}}, "title": "QuestionUpdateRequest", "type": "object"}, "_QuestionBatchUpdateData": {"description": "JSON:API data wrapper for a single item in a batch question update.", "properties": {"type": {"const": "questions", "description": "Must be \"questions\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the question to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/QuestionUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_QuestionBatchUpdateData", "type": "object"}, "QuestionDeleteRequestEnvelope": {"description": "JSON:API envelope for DELETE /v2/questions/ batch delete.\n\nThe ``data`` field is always an array \u2014 single-delete is a list of one.\nCap enforcement (<=100 items) lives in the endpoint so violations produce\nHTTP 400 via ``ValidationError`` instead of Pydantic's 422.", "properties": {"data": {"description": "List of question items to delete.", "items": {"$ref": "#/components/schemas/_QuestionDeleteData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "QuestionDeleteRequestEnvelope", "type": "object"}, "_QuestionDeleteData": {"description": "JSON:API resource-identifier object for a single question delete item.\n\nDelete items carry only ``type`` and ``id`` \u2014 no ``attributes`` \u2014 mirroring\nthe resource-identifier SHAPE (``type`` + ``id``) of ``_QuizDeleteData`` and\n``api_v2.groups.schemas._GroupDeleteData``, not the latter's field-level\nvalidation (its ``id`` ``max_length`` cap and blank-id validator are not\nported here). A question is homed under a quiz XOR a bank, but delete only\nneeds the question's own obfuscated id \u2014 the parent is irrelevant to\nresolving it.", "properties": {"type": {"const": "questions", "description": "Must be \"questions\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the question to delete.", "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "_QuestionDeleteData", "type": "object"}, "QuestionDetailResponse": {"description": "Single-resource envelope for GET /v2/questions/{id}/ responses.", "properties": {"data": {"$ref": "#/components/schemas/QuestionResource"}}, "required": ["data"], "title": "QuestionDetailResponse", "type": "object"}, "PublishedCourseFilters": {"description": "Query parameters for GET /v2/published-courses.", "properties": {"filter[domain]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque domain id. Returns only published courses on that domain. Unknown ids match nothing.", "title": "Filter[Domain]"}, "filter[course]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque course id. Returns only published courses for that course. Unknown ids match nothing.", "title": "Filter[Course]"}, "filter[live]": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Filter by published (live) state. Omit to return both live and unpublished rows.", "title": "Filter[Live]"}, "include": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated related resources to include. Supported: course, domain.", "title": "Include"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "PublishedCourseFilters", "type": "object"}, "PaginatedListResponse_PublishedCourseResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/PublishedCourseResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[PublishedCourseResource]", "type": "object"}, "PublishedCourseAttributes": {"description": "Attributes of a published-course resource object.", "properties": {"external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "slug": {"description": "URL path of the course on the domain. Unique per domain.", "title": "Slug", "type": "string"}, "live": {"description": "Whether the course is currently published (live) on the domain.", "title": "Live", "type": "boolean"}, "is_hidden": {"description": "Hidden from the catalog; visible only to associated groups.", "title": "Is Hidden", "type": "boolean"}, "visible_on_catalog": {"description": "Whether the course shows on the domain catalog.", "title": "Visible On Catalog", "type": "boolean"}, "open_access": {"description": "Allow anonymous access (no registration). Disables per-user analytics when true.", "title": "Open Access", "type": "boolean"}, "strict_enforce_group_visibility": {"description": "Block access for students the course is not visible to.", "title": "Strict Enforce Group Visibility", "type": "boolean"}, "visibility_override_type": {"description": "Which visibility mode the course uses: GROUP or CATEGORY.", "enum": ["GROUP", "CATEGORY"], "title": "Visibility Override Type", "type": "string"}, "access_period_starts_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Start of the registration/enrollment window, or null.", "title": "Access Period Starts At"}, "access_period_ends_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "End of the registration/enrollment window, or null.", "title": "Access Period Ends At"}, "restrict_access_start_end_dates": {"description": "Enforce the access window above.", "title": "Restrict Access Start End Dates", "type": "boolean"}, "allow_self_service_reenroll": {"description": "Let students re-enroll immediately on completion.", "title": "Allow Self Service Reenroll", "type": "boolean"}, "unique_progress_per_enrollment": {"description": "New progress record on each re-enroll.", "title": "Unique Progress Per Enrollment", "type": "boolean"}, "require_all_prerequisites": {"description": "Require all prerequisites before enrolling.", "title": "Require All Prerequisites", "type": "boolean"}, "created_at": {"description": "Creation timestamp.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Latest update timestamp.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["external_id", "slug", "live", "is_hidden", "visible_on_catalog", "open_access", "strict_enforce_group_visibility", "visibility_override_type", "restrict_access_start_end_dates", "allow_self_service_reenroll", "unique_progress_per_enrollment", "require_all_prerequisites", "created_at", "modified_at"], "title": "PublishedCourseAttributes", "type": "object"}, "PublishedCourseResource": {"description": "JSON:API resource object for a published course.", "properties": {"type": {"default": "published-courses", "description": "Always \"published-courses\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque published-course ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/PublishedCourseAttributes"}, "relationships": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Related course and domain linkages.", "title": "Relationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self link.", "title": "Links"}}, "required": ["id", "attributes"], "title": "PublishedCourseResource", "type": "object"}, "BatchResultEnvelope_PublishedCourseResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_PublishedCourseResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[PublishedCourseResource]", "type": "object"}, "BatchSucceededItem_PublishedCourseResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/PublishedCourseResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[PublishedCourseResource]", "type": "object"}, "DomainIdentifier": {"description": "JSON:API resource identifier for a domain.", "properties": {"type": {"const": "domains", "description": "Must be \"domains\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque domain id.", "title": "Id", "type": "string"}}, "required": ["type", "id"], "title": "DomainIdentifier", "type": "object"}, "DomainRelationship": {"description": "To-one linkage to the domain the course is published to.", "properties": {"data": {"$ref": "#/components/schemas/DomainIdentifier"}}, "required": ["data"], "title": "DomainRelationship", "type": "object"}, "PublishRelationships": {"description": "Required relationships on publish: the course and the domain.", "properties": {"course": {"$ref": "#/components/schemas/CourseRelationship"}, "domain": {"$ref": "#/components/schemas/DomainRelationship"}}, "required": ["course", "domain"], "title": "PublishRelationships", "type": "object"}, "PublishedCourseCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/published-courses (publish one or many).\n\nCap enforcement (<= ``BATCH_MAX_ITEMS``) lives in the endpoint so it surfaces\nas HTTP 400, matching the v2 batch-response convention.", "properties": {"data": {"description": "Published-course items to create.", "items": {"$ref": "#/components/schemas/_PublishedCourseCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "PublishedCourseCreateRequestEnvelope", "type": "object"}, "PublishedCoursePublishAttributes": {"additionalProperties": false, "description": "Optional settings accepted on publish (POST). All omitted fields use model defaults.", "properties": {"slug": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL path on the domain. Auto-generated if omitted. Lowercase letters/numbers/dashes, 2+ parts.", "title": "Slug"}, "is_hidden": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide from the catalog. Default false.", "title": "Is Hidden"}, "visible_on_catalog": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Show on the domain catalog. Default false.", "title": "Visible On Catalog"}, "open_access": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Allow anonymous access. Default false.", "title": "Open Access"}, "strict_enforce_group_visibility": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Default false.", "title": "Strict Enforce Group Visibility"}, "visibility_override_type": {"anyOf": [{"enum": ["GROUP", "CATEGORY"], "type": "string"}, {"type": "null"}], "description": "Default GROUP.", "title": "Visibility Override Type"}, "access_period_starts_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Registration window start. Default null.", "title": "Access Period Starts At"}, "access_period_ends_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Registration window end. Default null.", "title": "Access Period Ends At"}, "restrict_access_start_end_dates": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Default false.", "title": "Restrict Access Start End Dates"}, "allow_self_service_reenroll": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Default false.", "title": "Allow Self Service Reenroll"}, "unique_progress_per_enrollment": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Default true.", "title": "Unique Progress Per Enrollment"}, "require_all_prerequisites": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Default true.", "title": "Require All Prerequisites"}}, "title": "PublishedCoursePublishAttributes", "type": "object"}, "_PublishedCourseCreateData": {"description": "JSON:API data object for one publish item.", "properties": {"type": {"const": "published-courses", "description": "Must be \"published-courses\".", "title": "Type", "type": "string"}, "attributes": {"allOf": [{"$ref": "#/components/schemas/PublishedCoursePublishAttributes"}]}, "relationships": {"$ref": "#/components/schemas/PublishRelationships"}}, "required": ["type", "relationships"], "title": "_PublishedCourseCreateData", "type": "object"}, "PublishedCourseUpdateAttributes": {"additionalProperties": false, "description": "Writable attributes on update (PATCH). ``slug`` is create-only and not accepted here.", "properties": {"is_hidden": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide from the catalog.", "title": "Is Hidden"}, "visible_on_catalog": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Show on the domain catalog.", "title": "Visible On Catalog"}, "open_access": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Allow anonymous access.", "title": "Open Access"}, "strict_enforce_group_visibility": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Strict Enforce Group Visibility"}, "visibility_override_type": {"anyOf": [{"enum": ["GROUP", "CATEGORY"], "type": "string"}, {"type": "null"}], "description": "GROUP or CATEGORY.", "title": "Visibility Override Type"}, "access_period_starts_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Registration window start; null clears it.", "title": "Access Period Starts At"}, "access_period_ends_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Registration window end; null clears it.", "title": "Access Period Ends At"}, "restrict_access_start_end_dates": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Restrict Access Start End Dates"}, "allow_self_service_reenroll": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Allow Self Service Reenroll"}, "unique_progress_per_enrollment": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Unique Progress Per Enrollment"}, "require_all_prerequisites": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Require All Prerequisites"}}, "title": "PublishedCourseUpdateAttributes", "type": "object"}, "PublishedCourseUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/published-courses (update one or many).", "properties": {"data": {"description": "Published-course items to update.", "items": {"$ref": "#/components/schemas/_PublishedCourseUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "PublishedCourseUpdateRequestEnvelope", "type": "object"}, "_PublishedCourseUpdateData": {"description": "JSON:API data object for one update item.", "properties": {"type": {"const": "published-courses", "description": "Must be \"published-courses\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque id of the published course to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/PublishedCourseUpdateAttributes"}}, "required": ["type", "id", "attributes"], "title": "_PublishedCourseUpdateData", "type": "object"}, "PublishedCourseDetailResponse": {"description": "JSON:API single-resource response for a published course.", "properties": {"data": {"$ref": "#/components/schemas/PublishedCourseResource"}}, "required": ["data"], "title": "PublishedCourseDetailResponse", "type": "object"}, "DomainFilters": {"description": "Query parameters for GET /v2/domains.", "properties": {"filter[access]": {"anyOf": [{"enum": ["PUBLIC", "PRIVATE", "PRIVATE_CODE"], "type": "string"}, {"type": "null"}], "description": "Return only domains with this access mode: PUBLIC, PRIVATE, or PRIVATE_CODE.", "title": "Filter[Access]"}, "filter[name]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Exact hostname match. Returns the single domain with that name, if any.", "title": "Filter[Name]"}, "include": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated related resources to include. Supported: theme.", "title": "Include"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "DomainFilters", "type": "object"}, "DomainAttributes": {"description": "Attributes of a domain resource object.", "properties": {"external_id": {"description": "Stable UUID5 for cross-system correlation.", "title": "External Id", "type": "string"}, "name": {"description": "Hostname the domain serves. Unique per organization.", "title": "Name", "type": "string"}, "access": {"description": "Catalog access mode: PUBLIC, PRIVATE (login required), or PRIVATE_CODE (access code required).", "enum": ["PUBLIC", "PRIVATE", "PRIVATE_CODE"], "title": "Access", "type": "string"}, "access_message_html": {"description": "Message shown on the login / signup / access-code pages.", "title": "Access Message Html", "type": "string"}, "marketing_message": {"description": "Catalog marketing copy.", "title": "Marketing Message", "type": "string"}, "require_https": {"description": "Whether the domain forces HTTPS.", "title": "Require Https", "type": "boolean"}, "created_at": {"description": "Creation timestamp.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Latest update timestamp.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["external_id", "name", "access", "access_message_html", "marketing_message", "require_https", "created_at", "modified_at"], "title": "DomainAttributes", "type": "object"}, "DomainResource": {"description": "JSON:API resource object for a domain.", "properties": {"type": {"default": "domains", "description": "Always \"domains\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque domain ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/DomainAttributes"}, "relationships": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Related theme linkage.", "title": "Relationships"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self link.", "title": "Links"}}, "required": ["id", "attributes"], "title": "DomainResource", "type": "object"}, "PaginatedListResponse_DomainResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/DomainResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[DomainResource]", "type": "object"}, "DomainDetailResponse": {"description": "JSON:API single-resource response for a domain.", "properties": {"data": {"$ref": "#/components/schemas/DomainResource"}}, "required": ["data"], "title": "DomainDetailResponse", "type": "object"}, "BatchResultEnvelope_SignupFieldValueResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_SignupFieldValueResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[SignupFieldValueResource]", "type": "object"}, "BatchSucceededItem_SignupFieldValueResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/SignupFieldValueResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[SignupFieldValueResource]", "type": "object"}, "SignupFieldIdentifier": {"description": "JSON:API resource identifier for a signup field.", "properties": {"type": {"default": "signup-fields", "description": "Always \"signup-fields\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque signup-field ID.", "title": "Id", "type": "string"}}, "required": ["id"], "title": "SignupFieldIdentifier", "type": "object"}, "SignupFieldRelationship": {"description": "To-one linkage to a signup field.", "properties": {"data": {"$ref": "#/components/schemas/SignupFieldIdentifier"}}, "required": ["data"], "title": "SignupFieldRelationship", "type": "object"}, "SignupFieldValueAttributes": {"description": "Attributes of a signup-field-value resource.", "properties": {"label": {"description": "The signup field's display label.", "title": "Label", "type": "string"}, "value": {"description": "The student's answer for this field.", "title": "Value", "type": "string"}}, "required": ["label", "value"], "title": "SignupFieldValueAttributes", "type": "object"}, "SignupFieldValueRelationships": {"description": "Relationships of a signup-field-value resource.", "properties": {"student": {"$ref": "#/components/schemas/StudentRelationship"}, "signup-field": {"$ref": "#/components/schemas/SignupFieldRelationship"}}, "required": ["student", "signup-field"], "title": "SignupFieldValueRelationships", "type": "object"}, "SignupFieldValueResource": {"description": "JSON:API resource object for a signup field value.", "properties": {"type": {"default": "signup-field-values", "description": "Always \"signup-field-values\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque signup-field-value ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/SignupFieldValueAttributes"}, "relationships": {"$ref": "#/components/schemas/SignupFieldValueRelationships"}}, "required": ["id", "attributes", "relationships"], "title": "SignupFieldValueResource", "type": "object"}, "SignupFieldValueCreateRequestEnvelope": {"description": "Request envelope for bulk-creating signup field values.", "properties": {"student_id": {"description": "Obfuscated student ID.", "title": "Student Id", "type": "string"}, "data": {"items": {"$ref": "#/components/schemas/_SignupFieldValueCreateData"}, "title": "Data", "type": "array"}}, "required": ["student_id", "data"], "title": "SignupFieldValueCreateRequestEnvelope", "type": "object"}, "_SignupFieldValueCreateAttributes": {"properties": {"value": {"description": "The student's answer for this field.", "title": "Value", "type": "string"}}, "required": ["value"], "title": "_SignupFieldValueCreateAttributes", "type": "object"}, "_SignupFieldValueCreateData": {"properties": {"type": {"const": "signup-field-values", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated signup-field ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/_SignupFieldValueCreateAttributes"}}, "required": ["type", "id", "attributes"], "title": "_SignupFieldValueCreateData", "type": "object"}, "SignupFieldValueUpdateRequestEnvelope": {"description": "Request envelope for bulk-updating signup field values.", "properties": {"data": {"items": {"$ref": "#/components/schemas/_SignupFieldValueUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "SignupFieldValueUpdateRequestEnvelope", "type": "object"}, "_SignupFieldValueUpdateAttributes": {"properties": {"value": {"description": "The student's answer for this field.", "title": "Value", "type": "string"}}, "required": ["value"], "title": "_SignupFieldValueUpdateAttributes", "type": "object"}, "_SignupFieldValueUpdateData": {"properties": {"type": {"const": "signup-field-values", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated signup-field-value ID.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/_SignupFieldValueUpdateAttributes"}}, "required": ["type", "id", "attributes"], "title": "_SignupFieldValueUpdateData", "type": "object"}, "SignupFieldValueFilters": {"description": "Query parameters for GET /v2/signup-field-values/.", "properties": {"filter[student.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only values for the student with this opaque id. Unknown ids match nothing.", "title": "Filter[Student.Id]"}, "filter[signup-field.id]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Return only values for the signup field with this opaque id. Unknown ids match nothing.", "title": "Filter[Signup-Field.Id]"}, "filter[domains]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comma-separated domain names; returns only values whose signup field is in one of those domains. Unknown names match nothing, never error.", "title": "Filter[Domains]"}, "page[size]": {"default": 25, "description": "Maximum number of records to return.", "maximum": 100, "minimum": 1, "title": "Page[Size]", "type": "integer"}, "page[cursor]": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor from the previous page's links.next.", "title": "Page[Cursor]"}}, "title": "SignupFieldValueFilters", "type": "object"}, "PaginatedListResponse_SignupFieldValueResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/SignupFieldValueResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[SignupFieldValueResource]", "type": "object"}, "SignupFieldValueResponse": {"description": "JSON:API single-resource response for a signup field value.", "properties": {"data": {"$ref": "#/components/schemas/SignupFieldValueResource"}}, "required": ["data"], "title": "SignupFieldValueResponse", "type": "object"}, "PaginatedListResponse_WebPackageResource_": {"properties": {"data": {"description": "The resources on this page, in cursor order.", "items": {"$ref": "#/components/schemas/WebPackageResource"}, "title": "Data", "type": "array"}, "meta": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination metadata, e.g. {\"page_size\": 25}.", "title": "Meta"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Pagination links, e.g. {\"self\": \"...\", \"next\": \"...\", \"prev\": null}.", "title": "Links"}, "has_more": {"description": "Whether additional pages exist after this one.", "title": "Has More", "type": "boolean"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Opaque cursor to pass as ``page[cursor]`` for the next page; null on the last page.", "title": "Next Cursor"}, "included": {"anyOf": [{"items": {"additionalProperties": true, "type": "object"}, "type": "array"}, {"type": "null"}], "description": "Related resources requested via ?include=. Absent when no include was requested.", "title": "Included"}}, "required": ["data", "has_more"], "title": "PaginatedListResponse[WebPackageResource]", "type": "object"}, "WebPackageAttributes": {"description": "Attributes of a web package resource object.\n\n``type`` and ``state`` are deliberately typed as loose ``str`` rather than\n``Literal``, matching ``LessonAttributes.type``: a new choice added to\n``WebPackage.PACKAGE_TYPE_CHOICES`` must never break serialization of\nexisting rows. The valid values are documented in the field descriptions.\n\nNote ``type`` lives inside ``attributes`` even though JSON:API reserves\nthat name at the resource level. This follows the established v2 precedent\n(``LessonAttributes.type``) and is what GE-249998 AC2 asks for.", "properties": {"type": {"description": "Package format: SCORM, SCORM_2004, XAPI, AICC, or HTML.", "title": "Type", "type": "string"}, "state": {"description": "Ingestion state: PROCESSING while the package is being fetched and unpacked, READY once usable, ERROR if the archive could not be processed. Poll this field after create.", "title": "State", "type": "string"}, "title": {"description": "Best-guess title parsed from the package manifest.", "title": "Title", "type": "string"}, "display_name": {"description": "Server-derived label shown in the dashboard. Recomputed on every save from the title, state and archive filename \u2014 it is not directly settable.", "title": "Display Name", "type": "string"}, "base_path": {"description": "Base location in storage of the unpacked package content.", "title": "Base Path", "type": "string"}, "created_at": {"description": "Timestamp when the web package was created.", "format": "date-time", "title": "Created At", "type": "string"}, "modified_at": {"description": "Timestamp when the web package was last modified.", "format": "date-time", "title": "Modified At", "type": "string"}}, "required": ["type", "state", "title", "display_name", "base_path", "created_at", "modified_at"], "title": "WebPackageAttributes", "type": "object"}, "WebPackageResource": {"description": "JSON:API resource object for a web package.", "properties": {"type": {"const": "web-packages", "default": "web-packages", "description": "Always \"web-packages\".", "title": "Type", "type": "string"}, "id": {"description": "Opaque web package ID. Use in URL paths.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/WebPackageAttributes"}, "links": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Self and related links.", "title": "Links"}}, "required": ["id", "attributes"], "title": "WebPackageResource", "type": "object"}, "BatchResultEnvelope_WebPackageResource_": {"properties": {"data": {"description": "Per-item results in request order.", "items": {"anyOf": [{"$ref": "#/components/schemas/BatchSucceededItem_WebPackageResource_"}, {"$ref": "#/components/schemas/BatchFailedItem"}]}, "title": "Data", "type": "array"}, "summary": {"$ref": "#/components/schemas/BatchSummary", "description": "Aggregate counts."}}, "required": ["data", "summary"], "title": "BatchResultEnvelope[WebPackageResource]", "type": "object"}, "BatchSucceededItem_WebPackageResource_": {"properties": {"status": {"const": "succeeded", "default": "succeeded", "title": "Status", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Resource ID of the created or updated resource.", "title": "Id"}, "result": {"$ref": "#/components/schemas/WebPackageResource", "description": "Full resource object for the processed item."}}, "required": ["result"], "title": "BatchSucceededItem[WebPackageResource]", "type": "object"}, "WebPackageCreateRequest": {"description": "Request attributes for POST /v2/web-packages/.\n\n``content_url`` uses the shared ``ContentUrl`` type from GE-253536 rather\nthan redeclaring the https/well-formed rule. That check is syntactic only\nand runs at schema-parse time (422); the authoritative network safety runs\nat fetch time in the worker.", "properties": {"content_url": {"description": "https:// URL of the package archive. The server fetches and re-hosts it asynchronously; the created resource starts in state=PROCESSING.", "title": "Content Url", "type": "string"}, "title": {"description": "Title for the package.", "maxLength": 500, "minLength": 1, "title": "Title", "type": "string"}}, "required": ["content_url", "title"], "title": "WebPackageCreateRequest", "type": "object"}, "WebPackageCreateRequestEnvelope": {"description": "JSON:API envelope for POST /v2/web-packages/.\n\nThe ``data`` field is always an array \u2014 single-create is a list of one.\nCap enforcement (<= ``BATCH_MAX_ITEMS`` items) lives in the endpoint, NOT\nhere \u2014 that gives us HTTP 400 via ``ValidationError`` instead of Pydantic's\n422, per the v2 batch-response convention.", "properties": {"data": {"description": "List of web package items to create.", "items": {"$ref": "#/components/schemas/_WebPackageCreateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "WebPackageCreateRequestEnvelope", "type": "object"}, "_WebPackageCreateData": {"description": "JSON:API data wrapper for web package creation.", "properties": {"type": {"const": "web-packages", "description": "Must be \"web-packages\".", "title": "Type", "type": "string"}, "attributes": {"$ref": "#/components/schemas/WebPackageCreateRequest"}}, "required": ["type", "attributes"], "title": "_WebPackageCreateData", "type": "object"}, "WebPackageUpdateRequest": {"description": "Request attributes for PATCH /v2/web-packages/.\n\n``title`` is the only writable field. ``type``, ``state`` and ``base_path``\nare server-owned, and ``display_name`` is derived \u2014 ``WebPackage.save()``\nrecomputes it unconditionally, so accepting it here would publish a\ncontract that silently discards caller input.", "properties": {"title": {"description": "New title for the package.", "maxLength": 500, "minLength": 1, "title": "Title", "type": "string"}}, "required": ["title"], "title": "WebPackageUpdateRequest", "type": "object"}, "WebPackageUpdateRequestEnvelope": {"description": "JSON:API envelope for PATCH /v2/web-packages/.\n\nThe ``data`` field is always an array \u2014 single-update is a list of one. Cap\nenforcement lives in the endpoint, matching the create-endpoint convention.", "properties": {"data": {"description": "List of web package items to update.", "items": {"$ref": "#/components/schemas/_WebPackageUpdateData"}, "title": "Data", "type": "array"}}, "required": ["data"], "title": "WebPackageUpdateRequestEnvelope", "type": "object"}, "_WebPackageUpdateData": {"description": "JSON:API data wrapper for a web package update.", "properties": {"type": {"const": "web-packages", "description": "Must be \"web-packages\".", "title": "Type", "type": "string"}, "id": {"description": "Obfuscated ID of the web package to update.", "title": "Id", "type": "string"}, "attributes": {"$ref": "#/components/schemas/WebPackageUpdateRequest"}}, "required": ["type", "id", "attributes"], "title": "_WebPackageUpdateData", "type": "object"}, "WebPackageDetailResponse": {"description": "JSON:API single-resource response for a web package.", "properties": {"data": {"$ref": "#/components/schemas/WebPackageResource"}}, "required": ["data"], "title": "WebPackageDetailResponse", "type": "object"}}, "securitySchemes": {"JWTAuth": {"type": "http", "scheme": "bearer"}}}, "servers": []}