OpenAPI tanımı

Okul entegrasyon API'sinin makine tarafından okunabilir tanımı — OpenAPI 3.1, 960 satır. İnsan için yazılmış anlatım referans spesifikasyonda.

Bu sayfa dosyanın kendisidir. Aşağıdaki metin depodaki docs/api/openapi.yaml dosyasının birebir içeriğidir; kod üreteci ya da Swagger'a vermek için ham dosyayı indirin.

openapi: 3.1.0

info:
  title: Yoklama Okul Entegrasyon API'si
  version: "1.0.0"
  summary: Okulların yoklama sistemlerine veri sağlamak için implement edeceği referans arayüz
  description: |
    Okulların ve üniversitelerin kendi Öğrenci Bilgi Sistemlerinde (ÖBS/OBS/UBYS/LMS)
    implement edeceği standart arayüz. Bu arayüz mevcutsa, yoklama uygulamaları
    ekran kazıma (screen scraping), tarayıcı otomasyonu veya öğretmen şifresi saklama
    gibi yöntemlere ihtiyaç duymaz.

    Veri modeli OneRoster v1.2 Rostering'i taban alır; yoklama katmanı Ed-Fi'nin
    StudentSectionAttendanceEvent yapısından esinlenir (OneRoster'da yoklama kavramı yoktur).

    ÖNEMLİ — VERİ MİNİMİZASYONU:
    Bu API'de hiçbir biyometrik alan tanımlı değildir ve tanımlanmayacaktır.
    TCKN gibi hassas kimlik alanları istenmez; eşleştirme için öğrenci numarası yeterlidir.
    Yoklama uçlarına gönderilen tek şey karardır, ham veri değil.
  license:
    name: CC BY 4.0
    url: https://creativecommons.org/licenses/by/4.0/
  contact:
    name: Yoklama Projesi

servers:
  - url: https://{host}/yoklama-api/v1
    description: Kurumun kendi sunucusu
    variables:
      host:
        default: obs.ornek.edu.tr

tags:
  - name: discovery
    description: Yetenek keşfi
  - name: auth
    description: Kimlik doğrulama
  - name: roster
    description: Kurum, dönem, ders, kullanıcı, kayıt verileri (salt okunur)
  - name: schedule
    description: Haftalık program ve istisnalar (salt okunur)
  - name: attendance
    description: Yoklama oturumu ve kayıtları (okuma + yazma)

security:
  - oauth2: [roster.readonly]

paths:

  /.well-known/yoklama-configuration:
    get:
      tags: [discovery]
      summary: Yetenek keşfi
      description: |
        İstemci, kurumun hangi yetenekleri desteklediğini buradan öğrenir ve
        davranışını ona göre ayarlar. Kimlik doğrulama gerektirmez.
      security: []
      operationId: getConfiguration
      responses:
        "200":
          description: Yapılandırma
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Configuration" }

  /oauth/token:
    post:
      tags: [auth]
      summary: Erişim belirteci al (OAuth 2.0 Client Credentials)
      security: []
      operationId: getToken
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type: { type: string, enum: [client_credentials] }
                client_id: { type: string }
                client_secret: { type: string, format: password }
                scope:
                  type: string
                  example: "roster.readonly attendance.write"
      responses:
        "200":
          description: Belirteç
          content:
            application/json:
              schema:
                type: object
                required: [access_token, token_type, expires_in]
                properties:
                  access_token: { type: string }
                  token_type: { type: string, enum: [Bearer] }
                  expires_in: { type: integer, example: 3600 }
                  scope: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /orgs:
    get:
      tags: [roster]
      summary: Kurumlar (okul, fakülte, bölüm hiyerarşisi)
      operationId: listOrgs
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/ifNoneMatch"
      responses:
        "200":
          description: Kurum listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema:
                type: object
                required: [orgs]
                properties:
                  orgs:
                    type: array
                    items: { $ref: "#/components/schemas/Org" }
        "304": { description: Değişiklik yok }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /terms:
    get:
      tags: [roster]
      summary: Akademik dönemler
      operationId: listTerms
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
      responses:
        "200":
          description: Dönem listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [terms]
                properties:
                  terms:
                    type: array
                    items: { $ref: "#/components/schemas/Term" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /courses:
    get:
      tags: [roster]
      summary: Ders kataloğu
      operationId: listCourses
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
        - name: orgSourcedId
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Ders listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [courses]
                properties:
                  courses:
                    type: array
                    items: { $ref: "#/components/schemas/Course" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /sections:
    get:
      tags: [roster]
      summary: Şubeler / ders açılışları
      description: Bir dersin belirli bir dönemde açılan örneği. Yoklamanın bağlandığı temel varlıktır.
      operationId: listSections
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
        - name: termSourcedId
          in: query
          schema: { type: string }
        - name: orgSourcedId
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Şube listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [sections]
                properties:
                  sections:
                    type: array
                    items: { $ref: "#/components/schemas/Section" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /sections/{sourcedId}/students:
    get:
      tags: [roster]
      summary: Şubedeki öğrenciler
      description: Uygulamanın en sık kullandığı uç — tek çağrıda sınıf listesi.
      operationId: listSectionStudents
      parameters:
        - $ref: "#/components/parameters/sourcedId"
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: Öğrenci listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [users]
                properties:
                  users:
                    type: array
                    items: { $ref: "#/components/schemas/User" }
        "404": { $ref: "#/components/responses/NotFound" }

  /sections/{sourcedId}/teachers:
    get:
      tags: [roster]
      summary: Şubenin öğretim elemanları
      operationId: listSectionTeachers
      parameters:
        - $ref: "#/components/parameters/sourcedId"
      responses:
        "200":
          description: Öğretmen listesi
          content:
            application/json:
              schema:
                type: object
                required: [users]
                properties:
                  users:
                    type: array
                    items: { $ref: "#/components/schemas/User" }
        "404": { $ref: "#/components/responses/NotFound" }

  /teachers/{sourcedId}/sections:
    get:
      tags: [roster]
      summary: Öğretmenin dersleri
      description: Öğretmen uygulamaya girdiğinde o dönemki derslerini görmesi için.
      operationId: listTeacherSections
      parameters:
        - $ref: "#/components/parameters/sourcedId"
        - name: termSourcedId
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Şube listesi
          content:
            application/json:
              schema:
                type: object
                required: [sections]
                properties:
                  sections:
                    type: array
                    items: { $ref: "#/components/schemas/Section" }
        "404": { $ref: "#/components/responses/NotFound" }

  /classrooms:
    get:
      tags: [roster]
      summary: Derslikler
      operationId: listClassrooms
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
      responses:
        "200":
          description: Derslik listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [classrooms]
                properties:
                  classrooms:
                    type: array
                    items: { $ref: "#/components/schemas/Classroom" }

  /users:
    get:
      tags: [roster]
      summary: Kullanıcılar (öğrenci, öğretmen, yönetici)
      operationId: listUsers
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
        - name: role
          in: query
          schema: { type: string, enum: [student, teacher, administrator, guardian] }
        - name: orgSourcedId
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Kullanıcı listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [users]
                properties:
                  users:
                    type: array
                    items: { $ref: "#/components/schemas/User" }

  /enrollments:
    get:
      tags: [roster]
      summary: Ders kayıtları (kim hangi şubede, hangi rolle)
      operationId: listEnrollments
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
        - name: sectionSourcedId
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Kayıt listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [enrollments]
                properties:
                  enrollments:
                    type: array
                    items: { $ref: "#/components/schemas/Enrollment" }

  /schedules:
    get:
      tags: [schedule]
      summary: Haftalık ders programı
      operationId: listSchedules
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/since"
        - name: sectionSourcedId
          in: query
          schema: { type: string }
        - name: teacherSourcedId
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Program listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [schedules]
                properties:
                  schedules:
                    type: array
                    items: { $ref: "#/components/schemas/Schedule" }

  /schedules/exceptions:
    get:
      tags: [schedule]
      summary: Program istisnaları (tatil, telafi, derslik değişikliği)
      operationId: listScheduleExceptions
      parameters:
        - $ref: "#/components/parameters/since"
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
      responses:
        "200":
          description: İstisna listesi
          content:
            application/json:
              schema:
                type: object
                required: [exceptions]
                properties:
                  exceptions:
                    type: array
                    items: { $ref: "#/components/schemas/ScheduleException" }

  /attendance/sessions:
    post:
      tags: [attendance]
      summary: Yoklama oturumu oluştur
      description: |
        Idempotent. `sourcedId` istemci tarafından deterministik üretilir.
        Aynı `sourcedId` ile ikinci POST 200 döner ve yeni kayıt oluşmaz.
      operationId: createAttendanceSession
      security:
        - oauth2: [attendance.write]
      parameters:
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttendanceSession" }
      responses:
        "201":
          description: Oluşturuldu
          headers:
            Location:
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendanceSession" }
        "200":
          description: Zaten mevcut (idempotent)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendanceSession" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /attendance/sessions/{sourcedId}:
    get:
      tags: [attendance]
      summary: Oturum detayı
      operationId: getAttendanceSession
      security:
        - oauth2: [attendance.readonly]
      parameters:
        - $ref: "#/components/parameters/sourcedId"
      responses:
        "200":
          description: Oturum
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendanceSession" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [attendance]
      summary: Oturumu güncelle / kapat
      operationId: patchAttendanceSession
      security:
        - oauth2: [attendance.write]
      parameters:
        - $ref: "#/components/parameters/sourcedId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                endedAt: { type: string, format: date-time }
                status: { type: string, enum: [open, finalized, voided] }
                notes: { type: string, nullable: true }
      responses:
        "200":
          description: Güncellendi
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttendanceSession" }
        "409":
          description: |
            Düzeltme penceresi kapalı. Kurum politikası, kapatılmış oturumun
            ne kadar süre düzeltilebileceğini belirler.
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }

  /attendance/records:
    post:
      tags: [attendance]
      summary: Yoklama kayıtlarını gönder (toplu, idempotent upsert)
      description: |
        Anahtar: (sessionSourcedId, userSourcedId). Aynı çift yeniden gönderilirse
        kayıt güncellenir, çift kayıt oluşmaz. Maksimum 500 kayıt / istek.

        BİYOMETRİK VERİ YOKTUR. Gönderilen tek şey karardır.
      operationId: postAttendanceRecords
      security:
        - oauth2: [attendance.write]
      parameters:
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sessionSourcedId, records]
              properties:
                sessionSourcedId: { type: string }
                records:
                  type: array
                  maxItems: 500
                  items: { $ref: "#/components/schemas/AttendanceRecord" }
      responses:
        "200":
          description: Tümü kabul edildi
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BatchResult" }
        "207":
          description: Kısmi başarı
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BatchResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [attendance]
      summary: Yoklama kayıtlarını oku (mutabakat için)
      operationId: listAttendanceRecords
      security:
        - oauth2: [attendance.readonly]
      parameters:
        - name: sectionSourcedId
          in: query
          required: true
          schema: { type: string }
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: Kayıt listesi
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
          content:
            application/json:
              schema:
                type: object
                required: [records]
                properties:
                  records:
                    type: array
                    items: { $ref: "#/components/schemas/AttendanceRecordRead" }

components:

  securitySchemes:
    oauth2:
      type: oauth2
      description: |
        OAuth 2.0 Client Credentials. Öğretmen şifresi asla istenmez, asla saklanmaz.
        Yetki kurumdan gelir; iptal edilebilir ve denetlenebilir.
      flows:
        clientCredentials:
          tokenUrl: /yoklama-api/v1/oauth/token
          scopes:
            roster.readonly: Kurum, dönem, ders, şube, derslik, kullanıcı, kayıt, program okuma
            attendance.write: Yoklama oturumu ve kaydı oluşturma/güncelleme
            attendance.readonly: Yoklama kaydı okuma

  parameters:
    limit:
      name: limit
      in: query
      description: Sayfa başına kayıt (varsayılan 100, maksimum 1000)
      schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
    offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, default: 0 }
    since:
      name: since
      in: query
      description: Bu tarihten sonra dateLastModified değişen kayıtlar (artımlı çekim)
      schema: { type: string, format: date-time }
    sourcedId:
      name: sourcedId
      in: path
      required: true
      schema: { type: string }
    ifNoneMatch:
      name: If-None-Match
      in: header
      schema: { type: string }
    idempotencyKey:
      name: Idempotency-Key
      in: header
      description: İsteğin tekrarını güvenli kılan istemci üretimli anahtar
      schema: { type: string, maxLength: 128 }

  headers:
    XTotalCount:
      description: Filtreye uyan toplam kayıt sayısı
      schema: { type: integer }
    ETag:
      description: Koleksiyonun sürüm etiketi
      schema: { type: string }

  responses:
    Unauthorized:
      description: Geçersiz veya eksik belirteç
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Forbidden:
      description: Kapsam yetersiz veya kurum bu istemciyi bu kaynak için yetkilendirmemiş
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    NotFound:
      description: Kaynak bulunamadı
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    BadRequest:
      description: Geçersiz istek
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    RateLimited:
      description: Hız sınırı aşıldı
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }

  schemas:

    Problem:
      type: object
      description: RFC 9457 Problem Details
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: string }

    Base:
      type: object
      required: [sourcedId, status, dateLastModified]
      properties:
        sourcedId:
          type: string
          description: Kurumun kendi benzersiz kimliği
        status:
          type: string
          enum: [active, tobedeleted]
          description: Kayıtlar silinmez, pasifleştirilir
        dateLastModified: { type: string, format: date-time }

    Configuration:
      type: object
      required: [specVersion, endpoints, capabilities]
      properties:
        specVersion: { type: string, example: "1.0" }
        institution:
          type: object
          properties:
            name: { type: string }
            sourcedId: { type: string }
        endpoints:
          type: object
          properties:
            base: { type: string, format: uri }
            token: { type: string, format: uri }
        capabilities:
          type: object
          properties:
            roster.read: { type: boolean }
            schedules.read: { type: boolean }
            classrooms.read: { type: boolean }
            attendance.write: { type: boolean }
            attendance.read: { type: boolean }
            attendance.sessionModel: { type: string, enum: [single, sweeps] }
            delta.since: { type: boolean }
            etag: { type: boolean }
        attendanceStatuses:
          type: array
          items: { type: string }
        timezone: { type: string, example: "Europe/Istanbul" }
        contact: { type: string }

    Org:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [name, type]
          properties:
            name: { type: string }
            type:
              type: string
              enum: [institution, faculty, department, school, program]
            identifier: { type: string, nullable: true }
            parentSourcedId: { type: string, nullable: true }

    Term:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [title, type, startDate, endDate]
          properties:
            title: { type: string }
            type: { type: string, enum: [semester, term, schoolYear, summerSchool] }
            startDate: { type: string, format: date }
            endDate: { type: string, format: date }
            schoolYear: { type: string, example: "2026-2027" }
            orgSourcedId: { type: string }

    Course:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [title, courseCode]
          properties:
            title: { type: string }
            courseCode: { type: string }
            orgSourcedId: { type: string }
            credits: { type: number, nullable: true }
            ects: { type: number, nullable: true }
            theoreticalHours: { type: integer, nullable: true }
            practicalHours: { type: integer, nullable: true }

    Section:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [title, courseSourcedId, termSourcedId]
          properties:
            title: { type: string }
            sectionCode: { type: string, nullable: true }
            courseSourcedId: { type: string }
            termSourcedId: { type: string }
            orgSourcedId: { type: string }
            language: { type: string, default: "tr" }
            deliveryMode: { type: string, enum: [faceToFace, online, hybrid] }
            maxEnrollment: { type: integer, nullable: true }
            attendanceRequired: { type: boolean, nullable: true }
            attendanceThreshold:
              type: number
              minimum: 0
              maximum: 1
              nullable: true
              description: |
                Kurumun devam eşiği. YÖK devam kuralını birim kurullarına bıraktığı için
                ulusal sabit bir eşik yoktur; kurum kendi eşiğini burada bildirir.

    Classroom:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [name]
          properties:
            name: { type: string }
            building: { type: string, nullable: true }
            capacity: { type: integer, nullable: true }
            type: { type: string, enum: [classroom, amphitheater, laboratory, studio, online] }
            orgSourcedId: { type: string, nullable: true }

    User:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [role, givenName, familyName]
          description: |
            VERİ MİNİMİZASYONU: TCKN alanı tanımlı değildir ve istenmez.
            Eşleştirme için identifier (öğrenci/personel numarası) yeterlidir.
            Kurum ek alan döndürürse istemci bunları yok sayar ve saklamaz.
          properties:
            role: { type: string, enum: [student, teacher, administrator, guardian] }
            username: { type: string, nullable: true }
            givenName: { type: string }
            familyName: { type: string }
            identifier:
              type: string
              description: Öğrenci numarası veya personel sicil numarası
            email: { type: string, format: email, nullable: true }
            orgSourcedIds:
              type: array
              items: { type: string }
            grade: { type: integer, nullable: true }
            title: { type: string, nullable: true }
            enrollmentStatus: { type: string, nullable: true }
            guardianSourcedIds:
              type: array
              nullable: true
              items: { type: string }
              description: 18 yaş altı için veli rızası akışı gerekiyorsa

    Enrollment:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [sectionSourcedId, userSourcedId, role]
          properties:
            sectionSourcedId: { type: string }
            userSourcedId: { type: string }
            role: { type: string, enum: [student, teacher, assistant] }
            primary: { type: boolean, default: false }
            beginDate: { type: string, format: date, nullable: true }
            endDate: { type: string, format: date, nullable: true }

    Schedule:
      allOf:
        - $ref: "#/components/schemas/Base"
        - type: object
          required: [sectionSourcedId, dayOfWeek, startTime, endTime]
          properties:
            sectionSourcedId: { type: string }
            classroomSourcedId: { type: string, nullable: true }
            dayOfWeek:
              type: integer
              minimum: 1
              maximum: 7
              description: ISO 8601 — 1 = Pazartesi, 7 = Pazar
            startTime: { type: string, pattern: '^([01]\d|2[0-3]):[0-5]\d$' }
            endTime: { type: string, pattern: '^([01]\d|2[0-3]):[0-5]\d$' }
            recurrence: { type: string, enum: [weekly, biweekly, once], default: weekly }
            validFrom: { type: string, format: date, nullable: true }
            validUntil: { type: string, format: date, nullable: true }
            teacherSourcedIds:
              type: array
              items: { type: string }

    ScheduleException:
      type: object
      required: [sourcedId, date, type]
      properties:
        sourcedId: { type: string }
        scheduleSourcedId: { type: string }
        date: { type: string, format: date }
        type: { type: string, enum: [cancelled, makeup, roomChange, timeChange] }
        startTime: { type: string, nullable: true }
        endTime: { type: string, nullable: true }
        classroomSourcedId: { type: string, nullable: true }
        reason: { type: string, nullable: true }

    AttendanceSession:
      type: object
      required: [sourcedId, sectionSourcedId, teacherSourcedId, date]
      properties:
        sourcedId:
          type: string
          description: İstemci üretimli, deterministik. Idempotency anahtarı.
        sectionSourcedId: { type: string }
        classroomSourcedId: { type: string, nullable: true }
        scheduleSourcedId: { type: string, nullable: true }
        teacherSourcedId: { type: string }
        date: { type: string, format: date }
        startedAt: { type: string, format: date-time, nullable: true }
        endedAt: { type: string, format: date-time, nullable: true }
        sweepCount:
          type: integer
          minimum: 1
          default: 1
          description: Ders içinde yapılan tarama turu sayısı
        method:
          type: string
          enum: [face-1to1, proximity, qr, pin, manual, mixed]
        source: { type: string, example: "yoklama-app" }
        status: { type: string, enum: [open, finalized, voided], default: open }
        notes: { type: string, nullable: true }

    AttendanceRecord:
      type: object
      required: [userSourcedId, status]
      description: |
        BİYOMETRİK ALAN YOKTUR. Yüz şablonu, görüntü veya ham canlılık verisi
        bu API'de taşınmaz. Gönderilen tek şey karardır.
      properties:
        userSourcedId: { type: string }
        status:
          type: string
          enum: [present, absent, late, excused, partial]
        presenceRatio:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
          description: Onaylanan tur / toplam tur
        sweepResults:
          type: array
          nullable: true
          items: { type: boolean }
          description: Tur bazlı sonuçlar; erken çıkış tespiti için
        method:
          type: string
          nullable: true
          enum: [face-1to1, proximity, qr, pin, manual]
        verifiedAt: { type: string, format: date-time, nullable: true }
        flags:
          type: array
          items:
            type: string
            enum: [ERKEN_AYRILDI, GEC_KATILDI, MANUEL, SADECE_BLE, ITIRAZ_VAR]
        note: { type: string, nullable: true }

    AttendanceRecordRead:
      allOf:
        - $ref: "#/components/schemas/AttendanceRecord"
        - type: object
          properties:
            sessionSourcedId: { type: string }
            date: { type: string, format: date }
            dateLastModified: { type: string, format: date-time }

    BatchResult:
      type: object
      required: [accepted, rejected]
      properties:
        accepted: { type: integer }
        rejected: { type: integer }
        errors:
          type: array
          items:
            type: object
            properties:
              userSourcedId: { type: string }
              code:
                type: string
                enum: [user_not_found, user_not_enrolled, session_finalized, invalid_status, duplicate]
              message: { type: string }

Bu sayfa depodaki docs/api/openapi.yaml dosyasından scripts/belgeleri-uret.py ile üretilir. Elle düzenlenmez; değişiklik kaynağa yapılır.