openapi: 3.1.0
info:
  title: VeriKYC Public API
  version: "1.0.0"
  description: |
    Public REST API cho VeriKYC. Tài liệu này tập trung vào response envelope
    chuẩn `{ ok, idempotency, data }` và hành vi của header **`X-Dedup-Key`**
    áp dụng cho mọi HTTP status code của endpoint webhook ingest.
servers:
  - url: https://kyc.baylenvietnam.com
    description: Production
  - url: https://verif-insight-dash.lovable.app
    description: Published preview
security:
  - BearerApiKey: []
tags:
  - name: Webhooks
    description: Inbound webhook ingest + signature verify
  - name: Sessions
    description: KYC session CRUD
  - name: Verify
    description: Public verify lookup
paths:
  /api/public/v1/webhooks/ingest:
    post:
      tags: [Webhooks]
      summary: Ingest inbound webhook event
      x-required-roles: [admin, user]
      x-access: workspace
      description: |
        **Quyền truy cập:** workspace API key — role `admin` hoặc `user`.


        Nhận event từ provider/workspace, cập nhật `verify_state` của phiên KYC
        liên quan rồi fan-out tới các webhook đã đăng ký.

        ### Dedup key
        Mỗi request được gán một `dedup_key` duy nhất (scope: workspace) tính theo
        thứ tự ưu tiên:

        1. `body.event_id` → `id:<event_id>`
        2. Header `Idempotency-Key` → `id:<idempotency-key>`
        3. Fallback `sha256(raw body)` → `hash:<sha256>`

        ### Response envelope (mọi status code)
        ```json
        {
          "ok": true | false,
          "error": "string?",
          "message": "string?",
          "issues": { },
          "dedup_key": "string",
          "idempotency": {
            "replay": true | false,
            "status": "processed" | "replayed" | "in_flight",
            "dedup_key": "string",
            "first_seen_at": "ISO8601 | null"
          },
          "data": { } | null
        }
        ```

        ### Headers trả về (luôn có `X-Dedup-Key`)
        | Header | Khi nào xuất hiện | Giá trị |
        | --- | --- | --- |
        | `X-Dedup-Key` | **Mọi response** (kể cả 4xx/5xx) | dedup_key của request |
        | `X-Idempotent-Replay` | 200 processed/replayed, 409 in_flight | `"true"` \| `"false"` |
        | `X-Idempotency-Status` | 200 processed/replayed, 409 in_flight | `processed` \| `replayed` \| `in_flight` |
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string, maxLength: 200 }
          description: Ưu tiên thấp hơn `body.event_id`, cao hơn hash payload.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IngestRequest" }
            examples:
              completed:
                value:
                  event: session.completed
                  event_id: evt_2026_01_12_0001
                  session_id: 8b1f24a0-1a2b-4c3d-9e8f-aabbccddeeff
                  status: Approved
                  decision: { reason: ok }
      responses:
        "200":
          description: Processed hoặc Replayed (đã thấy dedup_key trước đó)
          headers:
            X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" }
            X-Idempotent-Replay: { $ref: "#/components/headers/XIdempotentReplay" }
            X-Idempotency-Status:
              schema: { type: string, enum: [processed, replayed] }
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/IngestProcessed"
                  - $ref: "#/components/schemas/IngestReplayed"
              examples:
                processed:
                  summary: Xử lý mới
                  value:
                    ok: true
                    dedup_key: "id:evt_2026_01_12_0001"
                    idempotency:
                      replay: false
                      status: processed
                      dedup_key: "id:evt_2026_01_12_0001"
                      first_seen_at: null
                    data:
                      event: session.completed
                      session_id: 8b1f24a0-1a2b-4c3d-9e8f-aabbccddeeff
                      verify_code: abc123def456
                      verify_state: verified
                      status: Approved
                      verify_expires_at: "2027-01-12T08:30:00Z"
                      revoked_at: null
                      check_url: https://kyc.baylenvietnam.com/check/abc123def456
                      verify_api_url: https://kyc.baylenvietnam.com/api/public/v1/verify/abc123def456
                      updated_at: "2026-01-12T08:30:00Z"
                replayed:
                  summary: Cached replay
                  value:
                    ok: true
                    dedup_key: "id:evt_2026_01_12_0001"
                    idempotency:
                      replay: true
                      status: replayed
                      dedup_key: "id:evt_2026_01_12_0001"
                      first_seen_at: "2026-01-12T08:30:00Z"
                    data:
                      event: session.completed
                      session_id: 8b1f24a0-1a2b-4c3d-9e8f-aabbccddeeff
                      verify_code: abc123def456
                      verify_state: verified
                      status: Approved
        "400":
          description: Body không phải JSON hợp lệ
          headers: { X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              example:
                ok: false
                error: invalid_json
                message: Body must be valid JSON
                dedup_key: "hash:9f8c…"
        "401":
          description: API key thiếu hoặc không hợp lệ
          headers: { X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              example:
                ok: false
                error: invalid_api_key
                message: Invalid or missing API key
        "403":
          description: |
            API key hợp lệ nhưng role không đủ quyền (`forbidden`). Đổi sang
            API key có role phù hợp (xem `required_roles` trong body).
          headers: { X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ForbiddenEnvelope" }
              example:
                ok: false
                error: forbidden
                message: "API key role 'user' is not allowed. Required: admin."
                required_roles: [admin]
                your_role: user

                dedup_key: "hash:9f8c…"
        "404":
          description: Không tìm thấy phiên KYC khớp `session_id`/`verify_code`
          headers: { X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              example:
                ok: false
                error: session_not_found
                message: No KYC session matches the given identifier in this workspace
                dedup_key: "id:evt_2026_01_12_0001"
        "409":
          description: Đang xử lý request khác cùng `dedup_key` (in-flight). Hãy retry.
          headers:
            X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" }
            X-Idempotent-Replay: { $ref: "#/components/headers/XIdempotentReplay" }
            X-Idempotency-Status:
              schema: { type: string, enum: [in_flight] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IngestInFlight" }
              example:
                ok: false
                error: idempotent_in_flight
                message: A request with this dedup_key is already being processed
                dedup_key: "id:evt_2026_01_12_0001"
                idempotency:
                  replay: true
                  status: in_flight
                  dedup_key: "id:evt_2026_01_12_0001"
                  first_seen_at: "2026-01-12T08:30:00Z"
                data: null
        "422":
          description: Body không pass schema validation
          headers: { X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              example:
                ok: false
                error: invalid_body
                message: Validation failed
                issues:
                  formErrors: []
                  fieldErrors:
                    event: ["Invalid enum value"]
                dedup_key: "hash:9f8c…"
        "500":
          description: Lỗi DB (idempotency log, lookup, hoặc update)
          headers: { X-Dedup-Key: { $ref: "#/components/headers/XDedupKey" } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              example:
                ok: false
                error: db_error
                message: Update failed
                dedup_key: "id:evt_2026_01_12_0001"

  /api/public/v1/verify-signature:
    post:
      tags: [Webhooks]
      summary: Verify HMAC-SHA256 signature of an outbound webhook payload
      x-required-roles: [admin]
      x-access: admin-only
      description: |
        **Quyền truy cập:** chỉ role `admin` (workspace owner/admin). Endpoint
        nội bộ phục vụ debug chữ ký webhook — không nên cấp cho người dùng cuối.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [secret, body, signature]
              properties:
                secret: { type: string }
                body: { type: string, description: Raw JSON body as string }
                signature: { type: string, description: Hex digest from X-Lovable-Signature }
      responses:
        "200":
          description: Result
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  expected: { type: string }

  /api/public/v1/sessions:
    post:
      tags: [Sessions]
      summary: Create a KYC session
      x-required-roles: [admin, user]
      x-access: workspace
      description: |
        **Quyền truy cập:** workspace API key — role `admin` hoặc `user`.
      responses:
        "200": { description: Session created }

  /api/public/v1/verify/{code}:
    get:
      tags: [Verify]
      summary: Lookup verify state by short code
      x-required-roles: []
      x-access: public
      description: |
        **Quyền truy cập:** công khai — không cần API key. Chỉ trả về dữ liệu
        verify an toàn cho người xem ngoài.
      security: []
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string }
      responses:
        "200": { description: Verify payload }
        "404": { description: Not found }


components:
  securitySchemes:
    BearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: lk_live_xxx
  headers:
    XDedupKey:
      description: Dedup key đã tính cho request (luôn echo lại trong mọi response).
      schema: { type: string }
    XIdempotentReplay:
      description: "`true` nếu request bị dedup (replayed/in_flight), `false` nếu mới xử lý."
      schema: { type: string, enum: ["true", "false"] }
  schemas:
    Idempotency:
      type: object
      required: [replay, status, dedup_key, first_seen_at]
      properties:
        replay: { type: boolean }
        status: { type: string, enum: [processed, replayed, in_flight] }
        dedup_key: { type: string }
        first_seen_at:
          type: string
          format: date-time
          nullable: true
    SessionData:
      type: object
      properties:
        event: { type: string }
        session_id: { type: string }
        verify_code: { type: string, nullable: true }
        verify_state:
          type: string
          enum: [verified, expired, revoked, pending, declined]
        status: { type: string, nullable: true }
        verify_expires_at: { type: string, format: date-time, nullable: true }
        revoked_at: { type: string, format: date-time, nullable: true }
        check_url: { type: string, nullable: true }
        verify_api_url: { type: string, nullable: true }
        updated_at: { type: string, format: date-time }
    IngestRequest:
      type: object
      required: [event]
      properties:
        event:
          type: string
          enum:
            - session.created
            - session.updated
            - session.completed
            - session.failed
            - session.expired
            - session.revoked
        event_id: { type: string, maxLength: 128 }
        session_id: { type: string, maxLength: 128 }
        verify_code: { type: string, pattern: "^[a-z0-9]+$", minLength: 6, maxLength: 32 }
        status: { type: string, maxLength: 64 }
        decision: { type: object, additionalProperties: true }
        verify_expires_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time }
      description: Cần ít nhất một trong `session_id` hoặc `verify_code`.
    IngestProcessed:
      type: object
      required: [ok, dedup_key, idempotency, data]
      properties:
        ok: { type: boolean, enum: [true] }
        dedup_key: { type: string }
        idempotency: { $ref: "#/components/schemas/Idempotency" }
        data: { $ref: "#/components/schemas/SessionData" }
    IngestReplayed:
      allOf:
        - $ref: "#/components/schemas/IngestProcessed"
      description: |
        Giống `IngestProcessed` nhưng `idempotency.replay = true`,
        `idempotency.status = "replayed"`, `idempotency.first_seen_at` ≠ null.
        `data` là payload đã cache của lần xử lý đầu tiên.
    IngestInFlight:
      type: object
      required: [ok, error, dedup_key, idempotency, data]
      properties:
        ok: { type: boolean, enum: [false] }
        error: { type: string, enum: [idempotent_in_flight] }
        message: { type: string }
        dedup_key: { type: string }
        idempotency: { $ref: "#/components/schemas/Idempotency" }
        data: { type: "null" }
    ErrorEnvelope:
      type: object
      required: [ok, error, dedup_key]
      properties:
        ok: { type: boolean, enum: [false] }
        error:
          type: string
          enum:
            - invalid_api_key
            - forbidden
            - invalid_json
            - invalid_body
            - session_not_found
            - db_error
        message: { type: string }
        issues:
          type: object
          description: Zod `flatten()` output, chỉ có khi `error = invalid_body`.
    ForbiddenEnvelope:
      type: object
      required: [ok, error, required_roles, your_role]
      properties:
        ok: { type: boolean, enum: [false] }
        error: { type: string, enum: [forbidden] }
        message: { type: string }
        required_roles:
          type: array
          items: { type: string, enum: [admin, user] }
        your_role: { type: string, enum: [admin, user] }

        dedup_key: { type: string }
