openapi: 3.0.3
info:
  title: Introduction
  version: "1.1"
  description: |
    Xporter exposes a compliant [IMS Global OneRoster v1.1](https://www.imsglobal.org/activity/oneroster) API
    that allows third-party systems to retrieve rostering data (organisations, users, classes, enrolments,
    and academic sessions) from schools connected to the platform.

    The Xporter web layer proxies requests to an internal Azure Functions back-end. Each function
    authenticates the caller by decrypting the Xporter bearer token and then executes a parameterised
    stored procedure against the identity database.

    ## Base URL

    ```
    https://xporter.groupcall.com/api/ims/oneroster/v1p1
    ```

    ## Architecture overview

    ```
    Integrator
        │
        │  POST /token  (Basic auth: client_id:client_secret)
        ▼
    Xporter Web (OneRosterController)
        │  forwards Xporter Bearer token + query string
        ▼
    Identity API (Azure Functions — OneRoster.cs)
        │  validates token via IdaasAuthorization.DecriptXodToken
        │  executes stored procedure via EntityFrameworkContext
        ▼
    Identity SQL Database (gcidm schema)
    ```

    ## Authentication

    This API uses **OAuth 2.0 client credentials flow**. Obtain a token first and include it in all
    subsequent requests.

    ### Step 1 — Obtain a bearer token

    Send a `POST` request to the `/token` endpoint with your `client_id` and `client_secret` in a
    **Basic Authorization** header:

    ```
    Authorization: Basic <base64(client_id:client_secret)>
    ```

    **Example request**

    ```http
    POST /api/ims/oneroster/v1p1/token HTTP/1.1
    Host: xporter.groupcall.com
    Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=
    ```

    **Successful response (200 OK)**

    ```json
    {
      "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
      "token_type": "Bearer",
      "expires_in": 3600
    }
    ```

    **Error response (401 Unauthorized)**

    ```json
    {
      "error": "invalid_client",
      "error_description": "Request contains an invalid client ID or secret"
    }
    ```

    ### Step 2 — Call protected endpoints

    Include the token in the `Authorization` header of every subsequent request:

    ```http
    Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
    ```

    ## Query parameter support

    All collection endpoints support the following query parameters. `sort`, `orderBy`, and `fields` are
    accepted but are not forwarded to the back-end and have no effect.

    | Parameter | Implemented | Description |
    |-----------|-------------|-------------|
    | `limit`   | ✅          | Maximum number of records to return. Omitting it returns all matching records. |
    | `offset`  | ✅          | Zero-based index of the first record to return. Use with `limit` for pagination. |
    | `filter`  | ✅ Partial  | Single `key='value'` expression. **Only `status` is supported.** The `enrollments` endpoint ignores this filter. |
    | `sort`    | ❌          | Accepted but ignored. |
    | `orderBy` | ❌          | Accepted but ignored. |
    | `fields`  | ❌          | Accepted but ignored. |

    **Filter syntax**

    ```
    filter=status='active'
    filter=status='tobedeleted'
    ```

    Only a single `key='value'` expression is supported. Complex `AND`/`OR` expressions are not parsed.

    ## Error responses

    Failed data requests return `400 Bad Request` with a **plain-text** body (the raw exception message),
    not JSON. Handle `400` responses accordingly.

    | Code | Scenario |
    |------|----------|
    | `200 OK` | Request succeeded. |
    | `400 Bad Request` | Token invalid/missing or server-side error. Body is plain text, not JSON. |
    | `401 Unauthorized` | Returned only by `POST /token` when client credentials are invalid. |
    | `403 Forbidden` | Bearer token absent on a collection endpoint. |

    ## Integration checklist

    - Obtain your `client_id` and `client_secret` from the Xporter platform.
    - Call `POST /token` and cache the `access_token` — reuse it until `expires_in` seconds have elapsed.
    - When `400` or `403` is received on a data endpoint, re-authenticate and retry once.
    - Use `limit` + `offset` to page through large result sets. Omitting `limit` returns all records.
    - Use `filter=status='active'` to exclude soft-deleted records where required.
    - Do **not** use `sort`, `orderBy`, or `fields` — these are silently ignored.
    - Expect `/courses` to always return `{ "courses": [] }` (Microsoft SDS stub only).
    - Link users to classes: join `users` → `enrollments` → `classes` via `sourcedId`.
    - Parent↔child relationships are carried in `User.agents`, not in a separate endpoint.

servers:
  - url: https://xporter.groupcall.com/api/ims/oneroster/v1p1
    description: Xporter host

tags:
  - name: Auth
    description: Obtain access tokens
  - name: Rostering
    description: OneRoster v1.1 rostering endpoints

paths:

  /version:
    get:
      summary: Get API version
      operationId: getVersion
      x-position: 8
      tags: [Rostering]
      description: Returns the OneRoster specification version string. Does not require authentication.
      security: []
      responses:
        "200":
          description: Version string
          content:
            text/plain:
              schema:
                type: string
                example: "1.1"

  /token:
    post:
      summary: Obtain a bearer token
      operationId: getToken
      x-position: 1
      tags: [Auth]
      description: |
        Authenticate using client credentials (OAuth 2.0 `client_credentials` grant).

        Provide your `client_id` and `client_secret` as a Basic Authorization header:

        ```
        Authorization: Basic <base64(client_id:client_secret)>
        ```

        On success, cache the `access_token` and reuse it until `expires_in` seconds
        have elapsed.
      security: []
      requestBody:
        required: false
        description: Optional body — credentials are taken from the Authorization header.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OauthTokenRequest"
      responses:
        "200":
          description: Token issued successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthTokenResponse"
              example:
                access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
                token_type: "Bearer"
                expires_in: 3600
        "401":
          description: Invalid client credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
              example:
                error: "invalid_client"
                error_description: "Request contains an invalid client ID or secret"

  /orgs:
    get:
      summary: Get all organisations
      operationId: getAllOrgs
      x-position: 2
      tags: [Rostering]
      description: |
        Returns all organisations (schools) the authenticated relying party has access to.


        **Note:** `parent` and `children` fields are not included in the response.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: Collection of organisations
          content:
            application/json:
              schema:
                type: object
                properties:
                  orgs:
                    type: array
                    items:
                      $ref: "#/components/schemas/Org"
              example:
                orgs:
                  - sourcedId: "100042"
                    status: "active"
                    dateLastModified: "2025-09-01T00:00:00"
                    identifier: "100042"
                    name: "Springfield Primary School"
                    type: "school"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /users:
    get:
      summary: Get all users
      operationId: getAllUsers
      x-position: 3
      tags: [Rostering]
      description: |
        Returns all users (staff and students) for the authenticated relying party.

        The `agents` array carries parent↔child relationships:
        - For **students**: contains GUIDRefs to parent/guardian user records.
        - For **parents/guardians**: contains GUIDRefs to child (student) user records.

        **Note:** `middleName`, `phone`, `sms`, `password`, and `enabledUser` are not returned.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: Collection of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
              example:
                users:
                  - sourcedId: "200001"
                    status: "active"
                    dateLastModified: "2025-09-01T00:00:00"
                    username: "jsmith"
                    givenName: "Jane"
                    familyName: "Smith"
                    identifier: "200001"
                    role: "student"
                    email: "jsmith@springfield.sch.uk"
                    orgs:
                      - sourcedId: "100042"
                        type: "school"
                    agents:
                      - sourcedId: "300010"
                        type: "User"
                    grades:
                      - "Year 7"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /classes:
    get:
      summary: Get all classes
      operationId: getAllClasses
      x-position: 4
      tags: [Rostering]
      description: |
        Returns all classes for the authenticated relying party.

        The `terms` array always contains exactly one entry (the associated academic session).

        **Note:** `course`, `grades`, `subjects`, `subjectCodes`, `periods`, `location`, and `resources` are not returned.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: Collection of classes
          content:
            application/json:
              schema:
                type: object
                properties:
                  classes:
                    type: array
                    items:
                      $ref: "#/components/schemas/Class"
              example:
                classes:
                  - sourcedId: "500123"
                    status: "active"
                    dateLastModified: "2025-09-01T00:00:00"
                    title: "Year 7 Maths Set 1"
                    classType: "scheduled"
                    school:
                      sourcedId: "100042"
                      type: "school"
                    terms:
                      - sourcedId: "400001"
                        type: "academicSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /enrollments:
    get:
      summary: Get all enrolments
      operationId: getAllEnrollments
      x-position: 5
      tags: [Rostering]
      description: |
        Returns all enrolments (user–class associations) for the authenticated relying party.

        **Note:** The `filter` parameter is accepted but the `status` value is **not forwarded**
        to the stored procedure for this endpoint — all enrolment records are returned regardless
        of status value.

        `beginDate` and `endDate` are not returned.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: Collection of enrolments
          content:
            application/json:
              schema:
                type: object
                properties:
                  enrollments:
                    type: array
                    items:
                      $ref: "#/components/schemas/Enrollment"
              example:
                enrollments:
                  - sourcedId: "ENR-99001"
                    status: "active"
                    dateLastModified: "2025-09-01T00:00:00"
                    role: "student"
                    user:
                      sourcedId: "200001"
                      type: "user"
                    class:
                      sourcedId: "500123"
                      type: "class"
                    school:
                      sourcedId: "100042"
                      type: "school"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /academicSessions:
    get:
      summary: Get all academic sessions
      operationId: getAllAcademicSessions
      x-position: 6
      tags: [Rostering]
      description: |
        Returns all academic sessions (school years / terms) for the authenticated relying party.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/filter"
      responses:
        "200":
          description: Collection of academic sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  academicSessions:
                    type: array
                    items:
                      $ref: "#/components/schemas/AcademicSession"
              example:
                academicSessions:
                  - sourcedId: "400001"
                    status: "active"
                    dateLastModified: "2025-09-01T00:00:00"
                    title: "2024/25"
                    type: "schoolYear"
                    startDate: "2024-09-01"
                    endDate: "2025-07-23"
                    schoolYear: "2025"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /courses:
    get:
      summary: Get all courses (stub)
      operationId: getAllCourses
      x-position: 7
      tags: [Rostering]
      description: |
        **Stub endpoint — always returns an empty array.**

        This endpoint exists for Microsoft SDS compatibility only. No course data
        is stored or returned. The response is always `{ "courses": [] }`.
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: Empty courses array (stub)
          content:
            application/json:
              schema:
                type: object
                properties:
                  courses:
                    type: array
                    items:
                      $ref: "#/components/schemas/Course"
                    maxItems: 0
              example:
                courses: []
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Bearer token obtained from `POST /token`.

  parameters:
    limit:
      name: limit
      in: query
      description: Maximum number of records to return. Omit to return all matching records.
      required: false
      schema:
        type: integer
        minimum: 1
        example: 100
    offset:
      name: offset
      in: query
      description: Zero-based index of the first record to return (pagination).
      required: false
      schema:
        type: integer
        minimum: 0
        default: 0
        example: 0
    filter:
      name: filter
      in: query
      description: |
        A single OneRoster filter expression. **Only `status` is supported as a filter key.**

        Supported values: `filter=status='active'` or `filter=status='tobedeleted'`

        Complex AND/OR expressions are not supported.
      required: false
      schema:
        type: string
        example: "status='active'"

  schemas:

    OauthTokenRequest:
      type: object
      description: Optional body for the token request. Credentials are taken from the Basic Auth header.
      properties:
        grant_type:
          type: string
          description: OAuth grant type. Only `client_credentials` is supported.
          example: client_credentials
        scope:
          type: string
          description: Requested scope (optional, currently unused).

    OauthTokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token:
          type: string
          description: Bearer token to include in subsequent requests.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
        token_type:
          type: string
          description: Always `"Bearer"`.
          example: Bearer
        expires_in:
          type: integer
          description: Seconds until the token expires.
          example: 3600

    OauthErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: invalid_client
        error_description:
          type: string
          example: Request contains an invalid client ID or secret

    GUIDRef:
      type: object
      description: A lightweight reference to another OneRoster resource.
      required: [sourcedId, type]
      properties:
        sourcedId:
          type: string
          description: The referenced resource's unique ID.
          example: "100042"
        type:
          type: string
          description: The resource type (e.g. `"school"`, `"class"`, `"user"`, `"academicSession"`).
          example: "school"

    Org:
      type: object
      description: |
        An organisation (school). `parent` and `children` fields are not returned.
      required: [sourcedId, status, dateLastModified, identifier, name, type]
      properties:
        sourcedId:
          type: string
          description: Unique numeric ID (as string).
          example: "100042"
        status:
          type: string
          enum: [active, tobedeleted]
          example: active
        dateLastModified:
          type: string
          format: date-time
          example: "2025-09-01T00:00:00"
        identifier:
          type: string
          description: School source ID — same value as `sourcedId`.
          example: "100042"
        name:
          type: string
          description: School display name.
          example: "Springfield Primary School"
        type:
          type: string
          description: OneRoster org type.
          example: "school"

    User:
      type: object
      description: |
        A user (student, teacher, or guardian).

        `middleName`, `phone`, `sms`, `password`, and `enabledUser` are not returned.

        The `agents` array carries parent↔child links: for students it contains parent/guardian
        GUIDRefs; for parents/guardians it contains child (student) GUIDRefs.
      required: [sourcedId, status, dateLastModified, username, givenName, familyName, identifier, role, orgs, agents, grades]
      properties:
        sourcedId:
          type: string
          example: "200001"
        status:
          type: string
          enum: [active, tobedeleted]
          example: active
        dateLastModified:
          type: string
          format: date-time
          example: "2025-09-01T00:00:00"
        username:
          type: string
          example: "jsmith"
        givenName:
          type: string
          example: "Jane"
        familyName:
          type: string
          example: "Smith"
        identifier:
          type: string
          description: Source system person ID — same value as `sourcedId`.
          example: "200001"
        role:
          type: string
          description: OneRoster role.
          enum: [student, teacher, administrator, aide, guardian, parent, proctor, relative]
          example: student
        email:
          type: string
          description: Home email address. May be null or empty.
          example: "jsmith@springfield.sch.uk"
        orgs:
          type: array
          description: Always one entry — the user's school.
          items:
            $ref: "#/components/schemas/GUIDRef"
          example:
            - sourcedId: "100042"
              type: "school"
        agents:
          type: array
          description: Parent or child user GUIDRefs. Type is always `"User"`.
          items:
            $ref: "#/components/schemas/GUIDRef"
        grades:
          type: array
          description: Year/grade group label(s). Single-element array or empty.
          items:
            type: string
          example: ["Year 7"]

    Class:
      type: object
      description: |
        A class/teaching group.

        `course`, `grades`, `subjects`, `subjectCodes`, `periods`, `location`, and `resources`
        are not returned.

        `terms` always contains exactly one entry.
      required: [sourcedId, status, dateLastModified, title, classType, school, terms]
      properties:
        sourcedId:
          type: string
          example: "500123"
        status:
          type: string
          enum: [active, tobedeleted]
          example: active
        dateLastModified:
          type: string
          format: date-time
          example: "2025-09-01T00:00:00"
        title:
          type: string
          example: "Year 7 Maths Set 1"
        classType:
          type: string
          enum: [scheduled, homeroom]
          example: scheduled
        school:
          allOf:
            - $ref: "#/components/schemas/GUIDRef"
          description: The school this class belongs to. Type is `"school"`.
        terms:
          type: array
          description: Academic session(s) for this class. Always contains exactly one entry. Type is `"academicSession"`.
          items:
            $ref: "#/components/schemas/GUIDRef"
          minItems: 1
          maxItems: 1

    Enrollment:
      type: object
      description: |
        A user–class enrolment record.

        `beginDate` and `endDate` are not returned.

        The `filter` parameter is not applied to this endpoint — all records are returned
        regardless of status.
      required: [sourcedId, status, dateLastModified, role, user, class, school]
      properties:
        sourcedId:
          type: string
          example: "ENR-99001"
        status:
          type: string
          enum: [active, tobedeleted]
          example: active
        dateLastModified:
          type: string
          format: date-time
          example: "2025-09-01T00:00:00"
        role:
          type: string
          enum: [student, teacher, administrator, aide, guardian, parent, proctor, relative]
          example: student
        user:
          allOf:
            - $ref: "#/components/schemas/GUIDRef"
          description: The enrolled user. Type is `"user"`.
        class:
          allOf:
            - $ref: "#/components/schemas/GUIDRef"
          description: The class the user is enrolled in. Type is `"class"`.
        school:
          allOf:
            - $ref: "#/components/schemas/GUIDRef"
          description: The school that owns this enrolment. Type is `"school"`.

    AcademicSession:
      type: object
      description: An academic session (school year, term, or semester).
      required: [sourcedId, status, dateLastModified, title, type, startDate, endDate, schoolYear]
      properties:
        sourcedId:
          type: string
          example: "400001"
        status:
          type: string
          enum: [active, tobedeleted]
          example: active
        dateLastModified:
          type: string
          format: date-time
          example: "2025-09-01T00:00:00"
        title:
          type: string
          example: "2024/25"
        type:
          type: string
          enum: [schoolYear, term, semester, gradingPeriod]
          example: schoolYear
        startDate:
          type: string
          description: Session start date as returned by the stored procedure.
          example: "2024-09-01"
        endDate:
          type: string
          description: Session end date as returned by the stored procedure.
          example: "2025-07-23"
        schoolYear:
          type: string
          description: Four-digit school year.
          example: "2025"

    Course:
      type: object
      description: Stub model — the courses endpoint always returns an empty array.
      properties:
        sourcedId:
          type: string

  responses:
    BadRequest:
      description: |
        Request failed — token invalid/missing, or a server-side error occurred.
        **The response body is plain text** (raw exception message), not JSON.
      content:
        text/plain:
          schema:
            type: string
          example: "Token validation failed"
    Forbidden:
      description: Bearer token absent at the web proxy layer.
      content:
        text/plain:
          schema:
            type: string
          example: "Forbidden"
