# Meram geliştirici API'si — DIŞ YÜZEY alt kümesi (üretilmiş; elle düzenlenmez).
# Kaynak: docs/api/openapi.yaml · üretici: infra/ci/scripts/openapi-redoc.mjs (S33-06) · etiketler: widget, widget-script, integration, centros.
# Yönetim API'si (Studio oturumu) bu dosyada yoktur; Studio › Kılavuz › Geliştirici API'si.
openapi: 3.1.0
info:
  title: Meram geliştirici API'si
  version: v1
  summary: Widget'sız istemciler, operatör uygulamaları ve yönetim otomasyonu için HTTP API
  description: |
    Üç kimlik modeli vardır; her uç yalnız birini kabul eder:

    1. **Widget oturumu** (`/api/widget/*`): sayfaya gömülü herkese açık anahtar (`mwpk_…`) ile oturum açılır,
       sonraki istekler oturum belirteciyle (`Authorization: Bearer <widget token>`) ve aynı `Origin` ile gider.
    2. **Entegrasyon anahtarı** (`mik_…`): sunucudan sunucuya çağrılar (devir bağlamı, ses olayları, Centros zarfı).
       `Authorization: Bearer mik_…` ya da `X-Meram-Integration-Key: mik_…`; her anahtar kapsam listesi taşır.
    3. **Studio oturumu**: `POST /auth/login` → access token (`Authorization: Bearer <access>`) + tenant seçimi;
       rol × uç matrisi `apps/engine/src/auth/rbac.ts`.

    Hata gövdeleri tek biçimdir: `{ statusCode, error, message }`; doğrulama hatasında `issues[]`
    (`path`, `code`, `message`; ham değer dönmez). Hız sınırı aşımı 429 + `Retry-After` (saniye).

    **Sürüm ve kullanımdan kaldırma.** `info.version` (`v1`) sözleşme sürümüdür; yollar sürüm öneki taşımaz.
    Aynı sürümde yalnız geriye uyumlu değişiklik yapılır (yeni uç / isteğe bağlı alan / yeni enum değeri);
    istemci bilmediği alanı yok saymalıdır. Kırıcı değişiklik yeni sürümle (`v2`, yol öneki `/v2/…`) ve en az 6 ay
    önceden duyuruyla gelir; kaldırılacak uç o süre boyunca `Deprecation` + `Sunset` yanıt başlıkları taşır.
    Ayrıntı: `docs/api/README.md` §12.

    Tam anlatım: `docs/api/README.md`.
  contact:
    name: Meram
servers:
  - url: https://{host}
    description: Engine adresi (müşteri kurulumuna göre; Studio gömme kodundaki `data-engine` değeri)
    variables:
      host:
        default: engine.example.com
tags:
  - name: widget
    description: Widget oturum API'si (herkese açık anahtar + oturum belirteci + izinli origin)
  - name: widget-script
    description: Widget betiği ve sürüm bildirimi (kimliksiz)
  - name: integration
    description: Entegrasyon anahtarıyla (`mik_…`) çağrılan sunucu uçları
  - name: centros
    description: Centros uyumlu zarf (`/api/botController`; v1 göçü)

paths:
  /api/widget/{publicKey}/session:
    post:
      tags: [widget]
      operationId: widgetCreateSession
      summary: Oturum aç (açılış turu + oturum belirteci)
      description: |
        Herkese açık anahtar (`mwpk_…`) + isteğin `Origin` başlığı anahtarın izinli origin listesinde olmalı.
        Yanıt açılış turunun mesajlarını, oturum kimliğini ve kayan ömürlü oturum belirtecini taşır. `streaming`
        `true` ise `…/message/stream` ucu açıktır; `voice` varsa ses ucu açıktır.

        Hız sınırı (60 sn pencere): IP başına 240 widget isteği, IP başına 20 yeni oturum. Ek olarak **anahtar
        başına** yeni oturum kotası (IP'den bağımsız; varsayılan 600 / dk ve 20 000 / gün, kurulum ayarı): aşımda
        429 `code: widget_key_quota` + `Retry-After` (günlük kotada saatler sürebilir). Origin reddi kotayı tüketmez.
        Bilinmeyen / iptal edilmiş / süresi dolmuş anahtar ve yayında olmayan akış aynı 404'ü alır.
      parameters:
        - $ref: '#/components/parameters/PublicKey'
        - $ref: '#/components/parameters/Origin'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetSessionBody'
            example:
              locale: tr
      responses:
        '201':
          description: Oturum açıldı
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetSessionResponse'
              example:
                sessionId: 3f1c2b7e-9a4d-4c1e-8b2f-5d6e7a8b9c0d
                messages:
                  - type: text
                    text: Merhaba, ben bir yapay zekâ asistanıyım.
                    ai: { label: ai-generated, source: flow }
                  - type: buttons
                    text: Size nasıl yardımcı olabilirim?
                    buttons:
                      - { label: Sipariş durumu, key: siparis }
                      - { label: Canlı destek, key: destek }
                state: { status: waiting }
                handoff: false
                token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzZjFjMmI3ZS0…
                expiresAt: '2026-11-04T10:30:00.000Z'
                bot: { name: Destek Asistanı, locale: tr }
                streaming: true
                voice: { maxAudioMs: 15000, binary: true, frames: true }
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/OriginDenied'
        '404':
          $ref: '#/components/responses/WidgetNotFound'
        '429':
          $ref: '#/components/responses/WidgetRateLimited'

  /api/widget/session/{sessionId}/message:
    post:
      tags: [widget]
      operationId: widgetMessage
      summary: Mesaj gönder (tam yanıt)
      description: |
        Metin, seçenek anahtarı (`key`), postback (`payload`) ya da form yanıtı (`form`) ile bir tur koşar. Yanıt
        yenilenmiş belirteci taşır (kayan 30 dk; oturumun mutlak üst ömrü 12 saat).

        `clientMessageId` verilirse aynı kimlikle gelen ikinci istek turu yeniden koşturmaz; biten ya da süren
        turun sonucunu döner (akışlı uçta kopma sonrası tam yanıta düşüş). Yeniden gönderim mesaj hız sınırına
        sayılmaz.

        Hız sınırı: IP başına 240 istek/dk; oturum başına 30 mesaj/dk.
      security:
        - widgetSession: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/Origin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetMessageBody'
            examples:
              metin:
                value: { text: Siparişim nerede?, clientMessageId: c-1a2b3c4d5e6f7a8b }
              secenek:
                value: { text: Sipariş durumu, key: siparis }
              form:
                value:
                  text: ''
                  form: { email: ornek@example.com, konu: Fatura }
      responses:
        '200':
          description: Tur yanıtı
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetTurnResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/WidgetUnauthorized'
        '403':
          $ref: '#/components/responses/OriginDenied'
        '404':
          $ref: '#/components/responses/WidgetNotFound'
        '429':
          $ref: '#/components/responses/WidgetRateLimited'

  /api/widget/session/{sessionId}/message/stream:
    post:
      tags: [widget]
      operationId: widgetMessageStream
      summary: Mesaj gönder (akışlı yanıt, SSE)
      description: |
        `message` ucunun akışlı eşi. Gövde ve denetimler aynıdır; denetimler akış açılmadan yapılır (hata → normal
        JSON hata kodu). Yanıt `text/event-stream`: `start` → (`partial` | `retract`)* → `final`. `final` verisi
        `message` ucunun yanıtıyla aynıdır. `partial` denetimden geçmiş tam anlık görüntüdür (parça değil);
        `retract` bir önizlemeyi geri çeker. Her 10 sn `: ping` yorum satırı gelir. Beklenmeyen hata `error` olayı.

        İstemci koparsa tur sürer; sonucu aynı `clientMessageId` ile `message` ucundan alın. Akış tenant'ta kapalıysa
        404 (`akışlı yanıt kapalı`) — `session` yanıtındaki `streaming` alanına bakın.

        `EventSource` `Authorization` başlığı taşıyamadığı için uç POST'tur; `fetch` + `ReadableStream` kullanın.
      security:
        - widgetSession: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/Origin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetMessageBody'
      responses:
        '200':
          description: Olay akışı
          content:
            text/event-stream:
              schema:
                type: string
                description: '`event: <ad>` + `data: <JSON>` çiftleri; olay verileri `WidgetStreamEvent`'
              example: |
                event: start
                data: {"clientMessageId":"c-1a2b3c4d5e6f7a8b"}

                event: partial
                data: {"id":"p1","text":"Siparişiniz kargoya"}

                event: final
                data: {"sessionId":"3f1c2b7e-9a4d-4c1e-8b2f-5d6e7a8b9c0d","messages":[{"type":"text","text":"Siparişiniz kargoya verildi.","ai":{"label":"ai-generated","source":"llm"}}],"state":{"status":"waiting"},"handoff":false,"token":"eyJ…","expiresAt":"2026-11-04T10:31:00.000Z"}
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/WidgetUnauthorized'
        '403':
          $ref: '#/components/responses/OriginDenied'
        '404':
          description: Akışlı yanıt kapalı ya da widget bulunamadı
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example: { statusCode: 404, error: NotFound, message: akışlı yanıt kapalı }
        '429':
          $ref: '#/components/responses/WidgetRateLimited'

  /api/widget/session/{sessionId}/operator-messages:
    get:
      tags: [widget]
      operationId: widgetOperatorMessages
      summary: Devir sonrası temsilci mesajlarını çek
      description: |
        Konuşma temsilciye devredildikten sonra (`handoff: true`) temsilcinin onaylı yanıtları bu uçtan yoklanır.
        `after` = son görülen `seq` (0 → hepsi); `gen` = son yanıttaki kutu nesli (opak). Nesil değiştiyse
        `cursorReset: true` ile kutunun tamamı döner; imleç dönen en büyük `seq`'e çekilmelidir. Yanıt belirteci
        yeniler. Yalnız IP hız sınırına sayılır (mesaj sınırına değil).
      security:
        - widgetSession: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/Origin'
        - name: after
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 1000000
        - name: gen
          in: query
          required: false
          schema:
            type: string
            pattern: '^[a-z0-9]{1,32}$'
      responses:
        '200':
          description: Temsilci mesajları
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetOperatorMessagesResponse'
              example:
                messages:
                  - {
                      id: m-01,
                      seq: 1,
                      type: operator,
                      text: 'Merhaba, size yardımcı olacağım.',
                      at: '2026-11-04T10:05:00.000Z',
                    }
                generation: g7k2
                token: eyJ…
                expiresAt: '2026-11-04T10:35:00.000Z'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/WidgetUnauthorized'
        '403':
          $ref: '#/components/responses/OriginDenied'
        '429':
          $ref: '#/components/responses/WidgetRateLimited'

  /api/widget/session/{sessionId}/voice:
    post:
      tags: [widget]
      operationId: widgetVoiceTurn
      summary: Sesli tur (STT → tur → TTS; NDJSON ya da ikili çerçeve)
      description: |
        Yazılı mesaj ucunun sesli eşi; kimlik ve hız sınırı aynıdır. Gövde ya JSON (`audio` = 16 kHz mono pcm16,
        base64) ya da `application/octet-stream` (ham pcm16; yerel ayar `?locale=`). Tek konuşma parçasının üst
        sınırı `session` yanıtındaki `voice.maxAudioMs` (varsayılan 15 sn).

        Yanıt `application/x-ndjson`: satır başına bir `WidgetVoiceEvent` — `transcript` → `turn` → `bot.speaking`
        → `bot.audio`* → `bot.speech_done`; ses metne çevrilemezse tek `stt.fallback`. `Accept:
        application/vnd.meram.voice-frames` ile giden ses base64'süz ikili çerçeve olarak gelir. İstemci bağlantıyı
        kapatırsa (barge-in) TTS akışı kesilir. Ses modu kapalıysa 404 (`ses modu kapalı`).
      security:
        - widgetSession: []
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/Origin'
        - name: locale
          in: query
          required: false
          description: Yalnız ham ikili gövdede (JSON gövdede `locale` alanı)
          schema:
            type: string
            maxLength: 10
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetVoiceBody'
          application/octet-stream:
            schema:
              type: string
              format: binary
              description: Ham 16 kHz mono pcm16 baytları
      responses:
        '200':
          description: Olay akışı (satır başına JSON) ya da ikili çerçeveler
          content:
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/WidgetVoiceEvent'
            application/vnd.meram.voice-frames:
              schema:
                type: string
                format: binary
        '400':
          description: Gövde / ses biçimi geçersiz ya da ses çok uzun
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                {
                  statusCode: 400,
                  error: BadRequest,
                  message: 'ses en çok 15 sn (16 kHz mono pcm16, base64)',
                }
        '401':
          $ref: '#/components/responses/WidgetUnauthorized'
        '403':
          $ref: '#/components/responses/OriginDenied'
        '404':
          description: Ses modu kapalı
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example: { statusCode: 404, error: NotFound, message: ses modu kapalı }
        '429':
          $ref: '#/components/responses/WidgetRateLimited'

  # ------------------------------------------------------------------------------------------ widget betiği

  /widget/manifest.json:
    get:
      tags: [widget-script]
      operationId: widgetManifest
      summary: Widget betiği sürüm bildirimi (sürüm + SRI özeti)
      description: Kimliksiz. Gömme kodu Studio'dan alınır; bu uç sürüm ve bütünlük özetini doğrulamak içindir.
      responses:
        '200':
          description: Bildirim
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /widget/{ver}/meram-widget.js:
    get:
      tags: [widget-script]
      operationId: widgetScript
      summary: Sürümlü widget betiği (SRI ile gömülür)
      parameters:
        - name: ver
          in: path
          required: true
          schema:
            type: string
            example: v0.1.0
      responses:
        '200':
          description: JavaScript
          content:
            application/javascript:
              schema:
                type: string
        '404':
          $ref: '#/components/responses/NotFound'

  # ------------------------------------------------------------------------------- entegrasyon anahtarı

  /integrations/handoff-context:
    get:
      tags: [integration]
      operationId: integrationHandoffContext
      summary: Devir bağlamını çek (operatör uygulaması)
      description: |
        Kapsam `handoff.context:read`. Konuşma `conversationId` | `sessionId` | `externalId`+`channel` ile bulunur
        (yalnız biri). `profile` verilmezse anahtarın varsayılan profili (`meram-default` kanonik şema,
        `centros` ya da tenant profili). Tenant yalnız anahtardan; başka tenant'ın konuşması 404. Devir yoksa 409.
        Hız sınıfı `default`: anahtar başına 300/dk ve tenant başına 1200/dk. Ayrıntı:
        `docs/integrations/handoff-context-api.md`.
      security:
        - integrationKey: []
        - integrationKeyHeader: []
      parameters:
        - name: conversationId
          in: query
          schema: { type: string, minLength: 1, maxLength: 128 }
        - name: sessionId
          in: query
          schema: { type: string, format: uuid }
        - name: externalId
          in: query
          schema: { type: string, minLength: 1, maxLength: 200 }
        - name: channel
          in: query
          description: '`externalId` ile zorunlu (ör. `centros`, `web`)'
          schema: { type: string, pattern: '^[a-z0-9_-]{1,32}$' }
        - name: bot
          in: query
          schema: { type: string, format: uuid }
        - name: profile
          in: query
          schema: { type: string, pattern: '^[a-z0-9][a-z0-9_-]{0,39}$' }
      responses:
        '200':
          description: Profil biçiminde devir bağlamı
          headers:
            X-Meram-Profile:
              schema: { type: string }
            X-Meram-Schema:
              schema: { type: string }
              description: '`meram.handoff-context/v1`'
            Cache-Control:
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HandoffContext'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/IntegrationUnauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Konuşmada devir yok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '429':
          $ref: '#/components/responses/IntegrationRateLimited'

  /integrations/widget/sessions:
    post:
      tags: [integration]
      operationId: integrationWidgetCreateSession
      summary: Uygulama oturumu aç (mobil / sunucu istemci; ADR-019)
      description: |
        Kapsam `widget:session` (varsayılan kapsamda yok; Studio › Ayarlar › Entegrasyonlar'da açıkça seçilir).
        **Arka uç → arka uç:** müşteri uygulaması (mobil / SPA / sunucu) widget anahtarı taşımaz; müşterinin kendi
        arka ucu bu uçla oturumu açar ve uygulamaya yalnız `sessionId` + `token` iletir. Sonraki istekler widget
        oturum uçlarıyla aynıdır (`/api/widget/session/{sessionId}/message`, `…/message/stream`,
        `…/operator-messages`, `…/voice`); bu belirteçlerde **`Origin` şartı yoktur** (belirteç origin yerine
        oturumu açan entegrasyon anahtarına bağlıdır; anahtarın iptali süren oturumu da düşürür, en geç 5 sn).
        Yanıt tarayıcı oturumuyla aynı şekildir (açılış turu, `bot`, `streaming`, `voice`, `appearance`, `texts`).

        `botKey`: widget anahtarı kaydının `id` değeri (Studio › Web widget listesi / `GET /widget-keys`) ya da
        herkese açık değeri (`mwpk_…`); yalnız anahtarın tenant'ında aranır — başka tenant'ın anahtarı, bilinmeyen,
        iptal edilmiş anahtar ve yayında olmayan akış aynı 404'ü alır. Akış kimliği kabul edilmez.
        `externalUserId`: müşteri arka ucunun kullanıcı kimliği (takma ad / CRM id; Meram'da ham saklanmaz) —
        devirde `GET /integrations/handoff-context?externalId=<değer>&channel=web` ile konuşma bulunur ve akışta
        `$user.info.params.externalUserId` olarak okunur. `attestation`: şemada ayrılmış kanca, bu sürümde 501.

        Sınırlar: entegrasyon anahtarı hız sınıfı `default` (anahtar 300/dk, tenant 1200/dk) + IP başına genel
        widget istek sınırı (IP başına oturum sınırı uygulanmaz: sunucu tek adresten çok kullanıcı açar) +
        **entegrasyon anahtarı başına** oturum kotası (widget anahtarı kotasıyla aynı varsayılanlar, ayrı sayaç;
        429 `code: widget_key_quota`). Bot için web kanalı kapalıysa 403 `channel-closed` (tarayıcı oturumuyla aynı
        kapı ve metin). Belirteç ömrü kayan 30 dk; mutlak tavan kurulum ayarı (varsayılan 12 saat).
      security:
        - integrationKey: []
        - integrationKeyHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [botKey]
              properties:
                botKey:
                  type: string
                  minLength: 8
                  maxLength: 200
                  description: Widget anahtarı kaydının `id` değeri (uuid) ya da herkese açık değeri (`mwpk_…`)
                locale:
                  type: string
                  maxLength: 10
                  description: 'İstemci dili (`tr`, `en`, `ar`…); yoksa akış dili'
                externalUserId:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: Müşteri arka ucunun kullanıcı kimliği (devir bağlamı harici id'si; isteğe bağlı)
                attestation:
                  type: object
                  description: Cihaz attestation kancası (ADR-019 §2.6); doğrulayıcı yok → 501
                  properties:
                    provider: { type: string, enum: [play-integrity, app-attest] }
                    token: { type: string }
            example:
              botKey: 7c9e6679-7425-40de-944b-e07fc1f90ae7
              locale: tr
              externalUserId: musteri-123
      responses:
        '201':
          description: Oturum açıldı (tarayıcı oturumuyla aynı gövde)
          headers:
            Cache-Control:
              schema: { type: string }
              description: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetSessionResponse'
              example:
                sessionId: 3f1c2b7e-9a4d-4c1e-8b2f-5d6e7a8b9c0d
                messages:
                  - type: text
                    text: Merhaba, ben bir yapay zekâ asistanıyım.
                    ai: { label: ai-generated, source: flow }
                state: { status: waiting }
                handoff: false
                token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzZjFjMmI3ZS0…
                expiresAt: '2026-11-05T10:30:00.000Z'
                bot: { name: Destek Asistanı, locale: tr }
                streaming: true
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/IntegrationUnauthorized'
        '403':
          description: Bot için web kanalı kapalı (`channel.enabled.web`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                {
                  statusCode: 403,
                  error: channel-closed,
                  message: Bu kanal şu anda hizmet vermiyor. Lütfen diğer iletişim kanallarımızı kullanın.,
                }
        '404':
          $ref: '#/components/responses/WidgetNotFound'
        '429':
          description: |
            Entegrasyon anahtarı hız sınırı, IP istek sınırı ya da anahtar başına oturum kotası
            (`code: widget_key_quota`); `Retry-After` saniye
          headers:
            Retry-After:
              schema: { type: integer }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitBody'
        '501':
          description: '`attestation` verildi; doğrulayıcı yok'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'

  /voice/calls/{callId}/events:
    post:
      tags: [integration]
      operationId: voiceCallEvent
      summary: Ses kanalı olayı gönder (telefon sağlayıcısı adaptörü)
      description: |
        Kapsam `voice:events`; hız sınıfı `voice` (anahtar başına 6000/dk). Gövde tek `VoiceInboundEvent`
        (`call.started`, `speech.final`, `audio`, `barge_in`, `dtmf`, `silence`, `playback.done`, `call.ended`…).
        Yanıt `application/x-ndjson`: giden olaylar satır satır (`bot.speaking`, `bot.audio`*, `bot.speech_done`,
        `call.transfer`, `call.hangup`, `call.error`); TTS parçaları üretildikçe akar, her 25 sn boş satır canlı
        tutma. Çağrı (tenant, callId) çiftiyle anahtarlanır. Ses kanalı kapalıysa (`VOICE_CHANNEL=off`) 404.
      security:
        - integrationKey: []
        - integrationKeyHeader: []
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
            pattern: '^[A-Za-z0-9_.:-]{1,96}$'
            example: call-7f3a9c
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceInboundEvent'
            examples:
              baslat:
                value: { type: call.started, flow: destek-hatti, locale: tr, provider: mock }
              konusma:
                value: { type: speech.final, text: Siparişim nerede }
      responses:
        '200':
          description: Giden olay akışı
          content:
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/VoiceOutboundEvent'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/IntegrationUnauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/IntegrationRateLimited'

  # ---------------------------------------------------------------------------------------------- centros

  /api/botController/token/{token}:
    post:
      tags: [centros]
      operationId: centrosTurn
      summary: Centros uyumlu tur (v1 ortus-bot zarfı)
      description: |
        URL ve gövde v1 `BotController` ile birebirdir (Centros `CHATBOT_URL`). Tur yanıtı `result.messages[]`;
        operatöre devir `tag: [connectOperator]`. Yanıt başlığı `X-Meram-Turn-Id` turun iz kimliğidir.

        Kimlik (ADR-018, `CENTROS_API_KEY` kipi): `observe` (varsayılan) anahtarı doğrular ama reddetmez; `enforce`
        kapsamı `centros:turn` olan entegrasyon anahtarı ister (`Authorization: Bearer mik_…` ya da
        `Meram-Api-Key: mik_…`): eksik / bilinmeyen / iptal → 401, kapsam yok ya da başka tenant → 403. Geçersiz zarf
        200 + nazik hata zarfı döner (Centros sözleşmesi). Ayrıntı: `docs/integrations/centros-api-anahtari.md`.
      security:
        - {}
        - integrationKey: []
        - centrosApiKey: []
      parameters:
        - name: token
          in: path
          required: true
          description: Yayındaki akışa bağlı kanal belirteci (v1 bot kimliği biçimi)
          schema:
            type: string
            example: v1bot-0a1b2c3d4e5f
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CentrosRequest'
            example:
              type: message
              recipientId: 9f8e7d6c5b4a3f2e1d0cchatbot
              message: { text: Merhaba, type: text }
              info: { params: { lang: tr, workChannel: web } }
      responses:
        '200':
          description: Centros yanıt zarfı
          headers:
            X-Meram-Turn-Id:
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CentrosResponse'
              example:
                result:
                  messages:
                    - { type: text, text: Merhaba, size nasıl yardımcı olabilirim? }
                    - type: array
                      values:
                        - { text: Sipariş durumu, extra: { key: siparis } }
                        - { text: Canlı destek, extra: { key: destek } }
                  conversation_id: 0123456789abcdef01234567
                  intent_name: karsilama
        '401':
          description: '`enforce` kipinde anahtar eksik / bilinmeyen / iptal'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '403':
          description: "`enforce` kipinde kapsam yok ya da anahtar botun tenant'ına ait değil"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '404':
          $ref: '#/components/responses/NotFound'

  # ------------------------------------------------------------------------------------------------- auth

components:
  securitySchemes:
    widgetSession:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Widget oturum belirteci (`POST /api/widget/{publicKey}/session` yanıtındaki `token`; her yanıt yeniler).
        HS256, `aud: meram-widget`; oturum kimliğine ve açıldığı `Origin`'e bağlıdır — başka origin'den 403.
        İstek `Origin` başlığı taşımalıdır (tarayıcı dışı istemci elle ekler).

        **`Origin` tarayıcı dışında kimlik değildir.** Tarayıcı bu başlığı zorlar; mobil uygulama, sunucu ya da
        `curl` istediğini yazar. Widget anahtarı (`mwpk_…`) da gizli değildir (sayfaya / uygulamaya gömülür). Bu
        yüzden widget oturumu **anonim son kullanıcı** kanalıdır: kötüye kullanım izinli origin listesi, kısa ömürlü
        belirteç, IP / oturum hız sınırı ve anahtar başına dakikalık + günlük oturum kotası (429 `code:
        widget_key_quota`) ile sınırlanır; anahtar Studio'dan iptal / döndürülür. Mobil / sunucu istemci için
        sunucu aracılı **uygulama oturumu** (ADR-019): müşteri arka ucu `widget:session` kapsamlı entegrasyon
        anahtarıyla `POST /integrations/widget/sessions` çağırır, uygulama yalnız oturum belirteci taşır; o
        belirteçler origin yerine entegrasyon anahtarına bağlıdır ve bu uçlarda `Origin` istenmez.
    integrationKey:
      type: http
      scheme: bearer
      description: 'Entegrasyon anahtarı `Authorization: Bearer mik_…` (`mik_` + 32 base64url karakter).'
    integrationKeyHeader:
      type: apiKey
      in: header
      name: X-Meram-Integration-Key
      description: Entegrasyon anahtarının başlık eşdeğeri (`Authorization` başka amaçla kullanılıyorsa).
    centrosApiKey:
      type: apiKey
      in: header
      name: Meram-Api-Key
      description: Centros bot ucunda entegrasyon anahtarı (`centros:turn` kapsamı; ADR-018).
  parameters:
    PublicKey:
      name: publicKey
      in: path
      required: true
      description: Widget herkese açık anahtarı (`mwpk_` + 24 base64url karakter)
      schema:
        type: string
        pattern: '^mwpk_[A-Za-z0-9_-]{24}$'
        example: mwpk_q7Zt3Lm9Xa2Pw8Rk1Vb4Yc6N
    SessionId:
      name: sessionId
      in: path
      required: true
      description: Oturum kimliği (`session` yanıtındaki `sessionId`; belirteçteki `sub` ile aynı olmalı)
      schema:
        type: string
        format: uuid
        example: 3f1c2b7e-9a4d-4c1e-8b2f-5d6e7a8b9c0d
    Origin:
      name: Origin
      in: header
      required: true
      description: '`http(s)://host[:port]`; anahtarın izinli listesinde olmalı, oturum boyunca aynı kalmalı'
      schema:
        type: string
        example: https://www.example.com
  responses:
    ValidationError:
      description: İstek gövdesi / sorgu şemaya uymuyor
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorBody'
          example:
            statusCode: 400
            error: ValidationError
            message: İstek gövdesi şemaya uymuyor
            issues:
              - {
                  path: text,
                  code: too_big,
                  message: 'Too big: expected string to have <=4000 characters',
                }
    WidgetUnauthorized:
      description: Belirteç yok / geçersiz / süresi dolmuş ya da widget anahtarı iptal edilmiş
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
          examples:
            yok:
              value: { statusCode: 401, error: Unauthorized, message: widget token gerekli }
            suresi-dolmus:
              value: { statusCode: 401, error: Unauthorized, message: widget token süresi doldu }
            iptal:
              value: { statusCode: 401, error: Unauthorized, message: widget anahtarı iptal edildi }
    OriginDenied:
      description: "`Origin` anahtarın izinli listesinde değil ya da belirteçteki origin'den farklı"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
          example: { statusCode: 403, error: Forbidden, message: bu origin için widget izni yok }
    WidgetNotFound:
      description: Anahtar bilinmiyor / iptal / süresi dolmuş ya da akış yayında değil (neden ayırt edilmez)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
          example: { statusCode: 404, error: NotFound, message: widget bulunamadı }
    WidgetRateLimited:
      description: |
        Hız sınırı (IP / oturum; 60 sn pencere) ya da anahtar başına oturum kotası (`code: widget_key_quota`;
        dakikalık + günlük pencere). `Retry-After` saniye. Mesaj isteğin diline göre (tr / en / ar).
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitBody'
          example:
            {
              statusCode: 429,
              error: TooManyRequests,
              message: 'Çok fazla istek; biraz sonra tekrar deneyin',
              retryAfterSec: 12,
            }
    IntegrationUnauthorized:
      description: Anahtar yok / biçimsiz / bilinmiyor / iptal / süresi dolmuş / kapsam yok — hepsi aynı 401
      headers:
        WWW-Authenticate:
          schema: { type: string }
          description: 'Bearer realm="meram-integration"'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
          example:
            { statusCode: 401, error: Unauthorized, message: Geçerli entegrasyon anahtarı gerekli }
    IntegrationRateLimited:
      description: Entegrasyon anahtarı hız sınırı (dakika penceresi; anahtar ve tenant sayaçları)
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitBody'
          example:
            {
              statusCode: 429,
              error: TooManyRequests,
              message: Entegrasyon anahtarı hız sınırı,
              retryAfterSec: 5,
            }
    NotFound:
      description: Kayıt yok (başka tenant'ın kaydı da 404)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
  schemas:
    ErrorBody:
      type: object
      required: [statusCode, error, message]
      properties:
        statusCode: { type: integer }
        error: { type: string }
        message: { type: string }
        code:
          type: string
          description: Bazı 400'lerde makine okunur alt kod (ör. `unknown-profile`)
    ValidationErrorBody:
      allOf:
        - $ref: '#/components/schemas/ErrorBody'
        - type: object
          properties:
            issues:
              type: array
              maxItems: 50
              items:
                type: object
                properties:
                  path: { type: string, description: 'Alan yolu (`a.b.0`)' }
                  code: { type: string, description: zod hata kodu }
                  message: { type: string }
    RateLimitBody:
      allOf:
        - $ref: '#/components/schemas/ErrorBody'
        - type: object
          properties:
            retryAfterSec: { type: integer, minimum: 1 }
            code:
              type: string
              enum: [widget_key_quota]
              description: |
                Yalnız widget anahtarı (`kid`) başına dakikalık / günlük oturum kotası aşımında (S33-06; uygulama
                oturumunda entegrasyon anahtarı başına aynı kota); IP / oturum hız sınırında alan yoktur. Son kullanıcı metni aynıdır (kota bilgisi sızmaz); istemci bu kodla
                "bekle ve yeniden dene" yerine "anahtar sahibine bildir" davranışı seçebilir.

    # ------------------------------------------------------------------------------------------- widget
    WidgetSessionBody:
      type: object
      properties:
        locale:
          type: string
          maxLength: 10
          description: 'İstemci dili (`tr`, `en`, `ar`…); yoksa akış dili'
    WidgetMessageBody:
      type: object
      description: '`text` boşsa `key`, `payload` ya da `form` olmalı (aksi 400 `boş mesaj`)'
      properties:
        text:
          type: string
          default: ''
          description: Serbest metin (kanal giriş sınırı tenant ayarı; mutlak tavan 4000 karakter)
        key:
          type: string
          minLength: 1
          maxLength: 200
          description: Seçilen düğmenin `key` değeri (`buttons` / `csat`)
        payload:
          type: string
          minLength: 1
          maxLength: 1000
          description: Kart düğmesi postback'i
        form:
          type: object
          maxProperties: 30
          additionalProperties: { type: string, maxLength: 1000 }
          description: '`form` mesajına yanıt (alan adı → değer)'
        locale: { type: string, maxLength: 10 }
        clientMessageId:
          type: string
          pattern: '^[A-Za-z0-9_-]{8,64}$'
          description: İstemci mesaj kimliği (yeniden gönderimde aynı tur sonucu; turu yeniden koşturmaz)
    WidgetVoiceBody:
      type: object
      required: [audio]
      properties:
        audio:
          type: string
          maxLength: 1400000
          description: 16 kHz mono pcm16, base64
        locale: { type: string, maxLength: 10 }
    WidgetAiLabel:
      type: object
      description: 'AI Act Md.50(2) makine okunur etiket; `source: llm` LLM üretimi, `flow` akışın yazılı metni'
      required: [label, source]
      properties:
        label: { type: string, enum: [ai-generated] }
        source: { type: string, enum: [llm, flow] }
    WidgetButton:
      type: object
      required: [label]
      properties:
        label: { type: string }
        key: { type: string, description: Seçenek anahtarı; tıklanınca `key` ile geri gönderilir }
        payload: { type: string, description: Postback; `payload` ile geri gönderilir }
        url: { type: string, description: Bağlantı düğmesi (http / https / tel) }
    WidgetCard:
      type: object
      required: [title, buttons]
      properties:
        title: { type: string }
        subtitle: { type: string }
        imageUrl: { type: string }
        url: { type: string }
        buttons:
          type: array
          items:
            $ref: '#/components/schemas/WidgetButton'
    WidgetOrder:
      type: object
      required: [orderNo]
      properties:
        orderNo: { type: string }
        name: { type: string }
        date: { type: string, description: ISO-8601 }
        quantity: { type: string }
        totalPrice: { type: string }
        status: { type: string }
        statusText: { type: string }
        pics:
          type: array
          items: { type: string }
    WidgetFormField:
      type: object
      required: [name, label, type, required]
      properties:
        name: { type: string, pattern: '^[A-Za-z_][A-Za-z0-9_]{0,63}$' }
        label: { type: string }
        type: { type: string, enum: [text, textarea, email, phone, number, date, select, checkbox] }
        required: { type: boolean }
        placeholder: { type: string }
        options:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              value: { type: string }
    WidgetMessage:
      type: object
      description: |
        Widget tel biçimi (kanal-bağımsız `Message` şemasının widget'a indirgenmiş hâli): `text`, `buttons`
        (`quickReplies`), `cards` (`cards` / `productArray`), `orders` (`orderArray`), `image`, `link` (`video` / `file`),
        `handoff`, `form`, `csat`. Her mesajda isteğe bağlı `ai` etiketi.
      required: [type]
      properties:
        type:
          { type: string, enum: [text, buttons, orders, cards, image, link, handoff, form, csat] }
        text: { type: string }
        buttons:
          type: array
          items:
            $ref: '#/components/schemas/WidgetButton'
        cards:
          type: array
          items:
            $ref: '#/components/schemas/WidgetCard'
        orders:
          type: array
          items:
            $ref: '#/components/schemas/WidgetOrder'
        url: { type: string, description: '`image` / `link`' }
        alt: { type: string }
        title: { type: string, description: '`form`' }
        fields:
          type: array
          items:
            $ref: '#/components/schemas/WidgetFormField'
        submitLabel: { type: string }
        scale: { type: string, enum: ['3', '5'], description: '`csat` ölçeği' }
        ai:
          $ref: '#/components/schemas/WidgetAiLabel'
    WidgetTurnResponse:
      type: object
      required: [sessionId, messages, state, handoff, token, expiresAt]
      properties:
        sessionId: { type: string, format: uuid }
        messages:
          type: array
          items:
            $ref: '#/components/schemas/WidgetMessage'
        state:
          type: object
          description: Tur durumu (akış çalışma zamanı; `status` ve isteğe bağlı bekleme bilgisi)
          additionalProperties: true
        handoff:
          type: boolean
          description: Konuşma temsilciye devredildi (sonrası `operator-messages` yoklanır)
        token: { type: string, description: Yenilenmiş oturum belirteci (kayan 30 dk) }
        expiresAt: { type: string, format: date-time }
        lang:
          type: string
          enum: [tr, en, ar]
          description: Yanıt müşterinin diline çevrildiyse hedef dil
    WidgetSessionResponse:
      allOf:
        - $ref: '#/components/schemas/WidgetTurnResponse'
        - type: object
          required: [bot]
          properties:
            bot:
              type: object
              properties:
                name: { type: string }
                locale: { type: string }
            streaming: { type: boolean, description: '`…/message/stream` ucu bu oturumda açık' }
            appearance:
              type: object
              description: Studio › Ayarlar › Widget (bot > tenant); ayar yoksa boş nesne. Gömme kodu `data-*` üstündür
              properties:
                title: { type: string, maxLength: 60 }
                subtitle: { type: string, maxLength: 80 }
                theme: { type: string, enum: [light, dark, auto] }
                accent: { type: string, pattern: '^#[0-9a-fA-F]{6}$' }
                avatarUrl:
                  { type: string, format: uri, maxLength: 512, description: Yalnız `https://` }
                placeholder: { type: string, maxLength: 80 }
                launcherLabel: { type: string, maxLength: 40 }
            voice:
              type: object
              description: Ses ucu açık (tenant / bot ayarı); yoksa alan gelmez
              properties:
                maxAudioMs: { type: integer }
                binary: { type: boolean, description: Ham pcm16 gövde kabul edilir }
                frames: { type: boolean, description: İkili çerçeve yanıtı istenebilir }
    WidgetOperatorMessagesResponse:
      type: object
      required: [messages, token, expiresAt]
      properties:
        messages:
          type: array
          items:
            type: object
            required: [id, seq, type, text, at]
            properties:
              id: { type: string }
              seq: { type: integer }
              type: { type: string, enum: [operator] }
              text: { type: string }
              at: { type: string, format: date-time }
        cursorReset: { type: boolean, description: İmleç geçersizdi; kutunun tamamı döndü }
        generation: { type: string, description: Kutu nesli (sonraki yoklamada `gen`) }
        token: { type: string }
        expiresAt: { type: string, format: date-time }
    WidgetVoiceEvent:
      description: NDJSON satırı (`type` ayırt eder)
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [transcript, stt.fallback, turn, bot.speaking, bot.audio, bot.speech_done]
        text:
          {
            type: string,
            description: '`transcript` dökümü / `stt.fallback` nazik metni / `bot.speaking` metni',
          }
        reason:
          type: string
          enum: [circuit-open, unavailable, timeout, error, empty]
          description: '`stt.fallback`'
        data:
          description: '`turn`: tam tur yanıtı; `bot.audio`: base64 ses parçası'
          oneOf:
            - $ref: '#/components/schemas/WidgetTurnResponse'
            - type: string
        utteranceId: { type: string }
        seq: { type: integer }
        format:
          type: object
          properties:
            encoding: { type: string }
            sampleRateHz: { type: integer }
            channels: { type: integer }
        via: { type: string, enum: [tts, text] }
        aborted: { type: boolean }

    # ---------------------------------------------------------------------------------- entegrasyon
    HandoffContext:
      type: object
      description: |
        Profil biçiminde devir bağlamı. `meram-default` profili kanonik `meram.handoff-context/v1` şemasıdır
        (konuşma kimlikleri, devir zamanı / nedeni, niyet, 3 satır özet, devir notu, maskeli değişkenler, isteğe bağlı
        son N tur); `centros` profili Centros Notlar penceresi biçimidir; tenant profili eşleme ile türetilir.
        Alan listesi `packages/schema/src/handoff-context.ts` ve `docs/integrations/handoff-context-api.md`.
      additionalProperties: true
    VoiceAudioFormat:
      type: object
      required: [encoding, sampleRateHz, channels]
      properties:
        encoding: { type: string, enum: [pcm16, opus, mp3, mulaw] }
        sampleRateHz: { type: integer, minimum: 8000, maximum: 48000 }
        channels: { type: integer, enum: [1, 2] }
    VoiceInboundEvent:
      type: object
      description: '`type` ayırt eder; alanlar türe göre (`packages/schema/src/voice-channel.ts`)'
      required: [type]
      properties:
        type:
          type: string
          enum:
            [
              call.started,
              speech.partial,
              speech.final,
              audio,
              barge_in,
              dtmf,
              silence,
              playback.done,
              call.ended,
            ]
        flow:
          {
            type: string,
            pattern: '^[a-z0-9][a-z0-9-]{0,63}$',
            description: "`call.started`: yayındaki akış slug'ı",
          }
        environment: { type: string }
        locale: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$' }
        caller: { type: string, description: 'Arayan (E.164); yalnız özeti saklanır' }
        provider: { type: string }
        inputFormat:
          $ref: '#/components/schemas/VoiceAudioFormat'
        outputFormat:
          $ref: '#/components/schemas/VoiceAudioFormat'
        text: { type: string, maxLength: 4000, description: '`speech.partial` / `speech.final`' }
        audio:
          { type: string, description: "`speech.final`: base64 ses (metin yoksa Meram STT'si)" }
        confidence: { type: number, minimum: 0, maximum: 1 }
        data: { type: string, description: '`audio`: base64 parça (≤ 900 000 karakter)' }
        digits: { type: string, pattern: '^[0-9*#]{1,32}$', description: '`dtmf`' }
        ms: { type: integer, description: '`silence`' }
        utteranceId: { type: string, description: '`playback.done`' }
        reason:
          type: string
          enum: [caller_hangup, bot_hangup, transferred, silence, error, timeout, provider]
          description: '`call.ended`'
    VoiceOutboundEvent:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [bot.speaking, bot.audio, bot.speech_done, call.transfer, call.hangup, call.error]
        utteranceId: { type: string }
        text: { type: string }
        options:
          type: array
          description: '`bot.speaking`: DTMF ile seçilebilir seçenekler'
          items:
            type: object
            properties:
              digit: { type: string }
              label: { type: string }
              key: { type: string }
              payload: { type: string }
        format:
          $ref: '#/components/schemas/VoiceAudioFormat'
        via: { type: string, enum: [tts, text] }
        seq: { type: integer }
        data: { type: string, description: '`bot.audio`: base64 TTS parçası' }
        aborted: { type: boolean }
        then: { type: string, enum: [listen, transfer, hangup] }
        reason: { type: string }
        target: { type: string }
        priority: { type: string }
        skills:
          type: array
          items: { type: string }
        conversationRef: { type: string }
        code: { type: string, description: '`call.error`' }

    # ---------------------------------------------------------------------------------------- centros
    CentrosRequest:
      type: object
      required: [message, recipientId]
      properties:
        type: { type: string, default: message }
        recipientId:
          type: string
          description: Oda / misafir kimliği (konuşma anahtarı)
        message:
          type: object
          properties:
            text: { type: [string, 'null'], maxLength: 8000 }
            type: { type: string, default: text }
            payload:
              description: Buton postback'i (metin ya da nesne)
              oneOf:
                - { type: string, maxLength: 4000 }
                - { type: object, additionalProperties: true }
            extra:
              type: object
              properties:
                key: { type: string, maxLength: 200, description: Seçenek anahtarı }
                type: { type: string }
              additionalProperties: true
        info:
          type: object
          description: '`token` (Centros oturum belirteci; geçirilir), `params` (misafir parametreleri; PII maskelenir), `url`'
          properties:
            token: { type: [string, 'null'] }
            params:
              type: object
              additionalProperties: true
            url: { type: string }
          additionalProperties: true
    CentrosMessage:
      type: object
      description: Centros `ChatBotMessage` (alan adları Java tarafıyla birebir)
      properties:
        type:
          type: string
          enum:
            [
              text,
              array,
              horizontalscrollarray,
              image,
              video,
              typing,
              json,
              custom,
              orderArray,
              productArray,
              redirect,
            ]
        text:
          description: Metin; `typing` için saniye; `image` / `video` için URL
          oneOf:
            - { type: string }
            - { type: number }
        values:
          type: array
          items:
            type: object
            additionalProperties: true
        values_source:
          type: object
          additionalProperties: true
        extra:
          type: object
          additionalProperties: true
        question: { type: string }
        askCount: { type: integer }
        waitUserInput: { type: boolean }
        url: { type: string }
        adata:
          type: object
          additionalProperties: true
    CentrosResponse:
      type: object
      required: [result]
      properties:
        result:
          type: object
          required: [messages]
          properties:
            messages:
              type: array
              items:
                $ref: '#/components/schemas/CentrosMessage'
            conversation_id: { type: string, description: 24 hex ya da `"null"` }
            intent_name: { type: [string, 'null'] }
            intent_id: { type: string }
            intent_score: { type: [string, 'null'] }
            current_flow_id: { type: string }
            tag:
              type: array
              description: '`connectOperator` → Centros operatöre bağlar'
              items:
                oneOf:
                  - { type: string }
                  - { type: integer }

    # --------------------------------------------------------------------------------------------- auth
