openapi: "3.1.0"
info:
  title: "Backslash Partners Guest API"
  description: |
      Public read APIs and inquiry intake for Backslash Partners (백슬래시파트너스).
      
      ## Versioning
      Preferred base URL uses path versioning: `https://backslash.co.kr/api/v1`. Breaking changes ship under `/api/v2/`. Successful responses include `API-Version: 1`.
      
      ## Deprecation policy
      Deprecated API versions send HTTP `Deprecation` and `Sunset` headers (RFC 8594) for at least 90 days before removal. Prefer the highest non-deprecated server from `servers`.
      
      ## Rate limits
      Versioned Guest API responses include `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (plus `X-RateLimit-*` aliases). HTTP 429 includes `Retry-After`.
      
      ## Pagination (list endpoints)
      Portfolio and blog list responses use cursor pagination: `{ data, pagination }` where `pagination` contains `limit`, `total`, `hasMore`, `nextCursor`, and `prevCursor`. Request the next page with `?cursor={nextCursor}` (optional `limit`). Legacy `page`/`size` query params are still accepted when `cursor` is omitted.
      
      Use `publicId` path parameters for portfolio/blog detail. Marketing site: https://backslash.co.kr. Agent index: https://backslash.co.kr/llms.txt. API docs: https://backslash.co.kr/docs. Developer resources: https://backslash.co.kr/developers.
  version: "1.0.0"
  contact:
    name: "Backslash Partners"
    email: |
        contact@backslash.co.kr
    url: |
        https://backslash.co.kr/contact
  license:
    name: "Proprietary"
    url: |
        https://backslash.co.kr/privacy
servers:
  - url: |
        https://backslash.co.kr/api/v1
    description: "Backslash Partners versioned Guest API (recommended for agents)"
  - url: |
        https://ec2-15-165-221-95.ap-northeast-2.compute.amazonaws.com
    description: |
        Origin guest API (same /guest/* resources)
  - url: |
        https://backslash.co.kr
    description: |
        Marketing site (OpenAPI, health, docs index)
tags:
  - name: "Portfolio"
    description: "Public portfolio list and detail for Backslash Partners"
  - name: "Blog"
    description: "Public blog/post list and detail"
  - name: "Inquiry"
    description: "Project inquiry intake from the marketing site"
  - name: "Site"
    description: "Machine-readable resources hosted on backslash.co.kr"
paths:
  "/guest/portfolio":
    get:
      tags:         - "Portfolio"
      operationId: "guestPortfolioSearch"
      summary: "Search public portfolios"
      description: |
          Returns a cursor-paginated list of public Backslash Partners portfolio items. Response shape: `{ data: Portfolio[], pagination: CursorPagination }`. Filter with isMain=true for homepage highlights. Prefer publicId from each item for detail fetches. On the versioned server this is GET /api/v1/guest/portfolio.
      parameters:
        - name: "isMain"
          in: "query"
          required: false
          schema:
            type: "boolean"
          description: |
              When true, only featured/main portfolios
        - name: "cursor"
          in: "query"
          required: false
          schema:
            type: "string"
          description: "Opaque pagination cursor from pagination.nextCursor or pagination.prevCursor on a prior response"
        - name: "limit"
          in: "query"
          required: false
          schema:
            type: "integer"
            minimum: 1
            maximum: 100
            default: 20
          description: |
              Page size (preferred). Default 20, maximum 100.
        - name: "page"
          in: "query"
          required: false
          schema:
            type: "integer"
            minimum: 0
            default: 0
          description: "Legacy zero-based page index. Ignored when cursor is set."
        - name: "size"
          in: "query"
          required: false
          schema:
            type: "integer"
            minimum: 1
            maximum: 100
          description: "Legacy page size alias. Prefer limit."
      responses:
        "200":
          description: "Cursor-paginated portfolio list"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/GuestPortfolioCursorPage
        "400":
          description: "Invalid query"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
        "429":
          description: "Rate limited — respect Retry-After"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
            Retry-After:
              description: "Seconds until the client may retry"
              schema:
                type: "integer"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
  "/guest/portfolio/{publicId}":
    get:
      tags:         - "Portfolio"
      operationId: "guestPortfolioGetByPublicId"
      summary: "Get portfolio detail by publicId"
      description: "Fetches one public portfolio by UUID publicId. Do not use sequential numeric IDs."
      parameters:
        - name: "publicId"
          in: "path"
          required: true
          schema:
            type: "string"
            format: "uuid"
          description: "Portfolio public UUID"
      responses:
        "200":
          description: "Portfolio detail"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/GuestPortfolioDetail
        "404":
          description: "Portfolio not found"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
        "429":
          description: "Rate limited"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
            Retry-After:
              schema:
                type: "integer"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
  "/guest/post":
    get:
      tags:         - "Blog"
      operationId: "guestPostSearch"
      summary: "Search public blog posts"
      description: |
          Cursor-paginated public posts for the Backslash Partners blog. Response shape: `{ data: Post[], pagination: CursorPagination }`. Optional isPopular and category filters.
      parameters:
        - name: "isPopular"
          in: "query"
          required: false
          schema:
            type: "boolean"
        - name: "category"
          in: "query"
          required: false
          schema:
            type: "string"
            enum:
              - "CATEGORY_TEST_1"
              - "CATEGORY_TEST_2"
              - "CATEGORY_TEST_3"
        - name: "cursor"
          in: "query"
          required: false
          schema:
            type: "string"
          description: "Opaque pagination cursor from pagination.nextCursor or pagination.prevCursor on a prior response"
        - name: "limit"
          in: "query"
          required: false
          schema:
            type: "integer"
            minimum: 1
            maximum: 100
            default: 20
          description: |
              Page size (preferred). Default 20, maximum 100.
        - name: "page"
          in: "query"
          required: false
          schema:
            type: "integer"
            minimum: 0
            default: 0
          description: "Legacy zero-based page index. Ignored when cursor is set."
        - name: "size"
          in: "query"
          required: false
          schema:
            type: "integer"
            minimum: 1
            maximum: 100
          description: "Legacy page size alias. Prefer limit."
      responses:
        "200":
          description: "Cursor-paginated post list"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/GuestPostCursorPage
        "429":
          description: "Rate limited"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
            Retry-After:
              schema:
                type: "integer"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
  "/guest/post/{publicId}":
    get:
      tags:         - "Blog"
      operationId: "guestPostGetByPublicId"
      summary: "Get blog post detail by publicId"
      description: "Fetches one public blog post by publicId (UUID). Prefer publicId over legacy numeric ids."
      parameters:
        - name: "publicId"
          in: "path"
          required: true
          schema:
            type: "string"
          description: "Post public identifier"
      responses:
        "200":
          description: "Post detail including related posts when available"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/GuestPostDetail
        "404":
          description: "Post not found"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
        "429":
          description: "Rate limited"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
            Retry-After:
              schema:
                type: "integer"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
  "/guest/inquiry":
    post:
      tags:         - "Inquiry"
      operationId: "guestInquiryCreate"
      summary: "Create a project inquiry"
      description: |
          Submits a project inquiry from the Backslash Partners marketing site. Requires privacyConsent=true. Typical response within one business day.
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              "$ref": |
                  #/components/schemas/GuestInquiryCreateReq
      responses:
        "200":
          description: "Inquiry accepted"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/GuestInquiryCreateResp
        "400":
          description: "Validation failed"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
        "429":
          description: "Rate limited"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
            Retry-After:
              schema:
                type: "integer"
          content:
            "application/json":
              schema:
                "$ref": |
                    #/components/schemas/ApiError
  "/openapi.json":
    servers:
      - url: |
            https://backslash.co.kr
    get:
      tags:         - "Site"
      operationId: "siteOpenApiJson"
      summary: "Backslash Partners OpenAPI document (JSON)"
      description: "Returns this OpenAPI 3.1 specification as application/json for agent discovery."
      responses:
        "200":
          description: "OpenAPI document"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                type: "object"
                additionalProperties: true
  "/api/openapi.yaml":
    servers:
      - url: |
            https://backslash.co.kr
    get:
      tags:         - "Site"
      operationId: "siteOpenApiYaml"
      summary: "Backslash Partners OpenAPI document (YAML)"
      description: "YAML encoding of the same guest OpenAPI document."
      responses:
        "200":
          description: "OpenAPI YAML"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/yaml":
              schema:
                type: "string"
  "/api/docs":
    servers:
      - url: |
            https://backslash.co.kr
    get:
      tags:         - "Site"
      operationId: "siteApiDocsIndex"
      summary: "Backslash Partners API docs index (JSON)"
      description: |
          Machine-readable index of Backslash Partners developer resources, versioning, and rate-limit conventions.
      responses:
        "200":
          description: "Developer resource index"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                type: "object"
                additionalProperties: true
  "/api/health":
    servers:
      - url: |
            https://backslash.co.kr
    get:
      tags:         - "Site"
      operationId: "siteHealth"
      summary: "Marketing site API health check"
      description: "Lightweight JSON health probe for agents and uptime monitors."
      responses:
        "200":
          description: "Service is up"
          headers:
            RateLimit-Limit:
              description: "Request quota for the current window"
              schema:
                type: "integer"
            RateLimit-Remaining:
              description: "Remaining requests in the current window"
              schema:
                type: "integer"
            RateLimit-Reset:
              description: "Seconds until the rate-limit window resets"
              schema:
                type: "integer"
            API-Version:
              description: "Active API major version"
              schema:
                type: "string"
                examples:                   - "1"
          content:
            "application/json":
              schema:
                type: "object"
                required:
                  - "status"
                  - "service"
                properties:
                  status:
                    type: "string"
                    examples:                       - "ok"
                  service:
                    type: "string"
                    examples:                       - "backslash-partners-web"
components:
  schemas:
    ApiError:
      type: "object"
      required:         - "error"
      properties:
        error:
          type: "object"
          required:
            - "code"
            - "message"
          properties:
            code:
              type: "string"
              description: "Machine-readable error code"
              examples:
                - "NOT_FOUND"
                - "BAD_REQUEST"
                - "METHOD_NOT_ALLOWED"
            message:
              type: "string"
              description: "Human-readable error summary"
            hint:
              type: "string"
              description: "How an agent or client should recover"
            details:
              type: "object"
              additionalProperties: true
    CursorPagination:
      type: "object"
      required:
        - "limit"
        - "total"
        - "hasMore"
        - "nextCursor"
        - "prevCursor"
      description: "Cursor pagination envelope for Backslash Partners list endpoints. Pass nextCursor or prevCursor as the cursor query parameter to fetch adjacent pages."
      properties:
        limit:
          type: "integer"
          minimum: 1
          maximum: 100
          description: "Number of items returned in this page (page size)"
        total:
          type: "integer"
          minimum: 0
          description: "Total matching items across all pages"
        hasMore:
          type: "boolean"
          description: "True when nextCursor is non-null"
        nextCursor:
          type: "string"
          nullable: true
          description: |
              Opaque cursor for the next page. Example: ?cursor={nextCursor}&limit={limit}
        prevCursor:
          type: "string"
          nullable: true
          description: "Opaque cursor for the previous page. Null on the first page."
    GuestPortfolioCursorPage:
      type: "object"
      required:
        - "data"
        - "pagination"
      properties:
        data:
          type: "array"
          items:
            "$ref": |
                #/components/schemas/GuestPortfolioListItem
        pagination:
          "$ref": |
              #/components/schemas/CursorPagination
    GuestPostCursorPage:
      type: "object"
      required:
        - "data"
        - "pagination"
      properties:
        data:
          type: "array"
          items:
            "$ref": |
                #/components/schemas/GuestPostListItem
        pagination:
          "$ref": |
              #/components/schemas/CursorPagination
    GuestFile:
      type: "object"
      properties:
        id:
          type: "integer"
          format: "int64"
        url:
          type: "string"
          format: "uri"
    GuestPortfolioListItem:
      type: "object"
      properties:
        publicId:
          type: "string"
          format: "uuid"
          description: "Opaque public identifier used in detail URLs"
        titleKo:
          type: "string"
        titleEng:
          type: "string"
        titleJp:
          type: "string"
          nullable: true
        summaryKo:
          type: "string"
        summaryEng:
          type: "string"
        summaryJp:
          type: "string"
          nullable: true
        descriptionKo:
          type: "string"
        descriptionEng:
          type: "string"
        descriptionJp:
          type: "string"
          nullable: true
        mainImageKo:
          "$ref": |
              #/components/schemas/GuestFile
        mainImageEng:
          "$ref": |
              #/components/schemas/GuestFile
        mainImageJp:
          "$ref": |
              #/components/schemas/GuestFile
    GuestPortfolioDetail:
      allOf:
        - type: "object"
          properties:
            publicId:
              type: "string"
              format: "uuid"
              description: "Opaque public identifier used in detail URLs"
            titleKo:
              type: "string"
            titleEng:
              type: "string"
            titleJp:
              type: "string"
              nullable: true
            summaryKo:
              type: "string"
            summaryEng:
              type: "string"
            summaryJp:
              type: "string"
              nullable: true
            descriptionKo:
              type: "string"
            descriptionEng:
              type: "string"
            descriptionJp:
              type: "string"
              nullable: true
            mainImageKo:
              "$ref": |
                  #/components/schemas/GuestFile
            mainImageEng:
              "$ref": |
                  #/components/schemas/GuestFile
            mainImageJp:
              "$ref": |
                  #/components/schemas/GuestFile
        - type: "object"
          properties:
            clientKo:
              type: "string"
            clientEng:
              type: "string"
            categoryKo:
              type: "string"
            categoryEng:
              type: "string"
            toolsKo:
              type: "string"
            toolsEng:
              type: "string"
            period:
              type: "string"
            imagesKo:
              type: "array"
              items:
                "$ref": |
                    #/components/schemas/GuestFile
            imagesEng:
              type: "array"
              items:
                "$ref": |
                    #/components/schemas/GuestFile
    GuestPostListItem:
      type: "object"
      properties:
        publicId:
          type: "string"
        titleKo:
          type: "string"
        titleEng:
          type: "string"
        contentKo:
          type: "string"
        contentEng:
          type: "string"
        isPopular:
          type: "boolean"
        isPublic:
          type: "boolean"
        createdAt:
          type: "string"
        mainImageKo:
          "$ref": |
              #/components/schemas/GuestFile
        mainImageEng:
          "$ref": |
              #/components/schemas/GuestFile
    GuestPostDetail:
      allOf:
        - "$ref": |
              #/components/schemas/GuestPostListItem
        - type: "object"
          properties:
            morePosts:
              type: "array"
              items:
                "$ref": |
                    #/components/schemas/GuestPostListItem
    GuestInquiryCreateReq:
      type: "object"
      required:
        - "name"
        - "contact"
        - "email"
        - "serviceDescription"
        - "privacyConsent"
      properties:
        name:
          type: "string"
          description: "Company / contact name"
        contact:
          type: "string"
          description: "Phone number"
        email:
          type: "string"
          format: "email"
        serviceDescription:
          type: "string"
          description: "What the client wants to build"
        privacyConsent:
          type: "boolean"
          description: "Must be true to accept the inquiry"
        budgetType:
          type: "string"
        funnelType:
          type: "string"
    GuestInquiryCreateResp:
      type: "object"
      properties:
        id:
          type: "integer"
          format: "int64"
x-backslash-locales:
  - "ko"
  - "en"
  - "jp"
x-backslash-versioning:
  strategy: "url-path"
  current: "v1"
  deprecationNoticeDays: 90
  headers:
    - "Deprecation"
    - "Sunset"
    - "API-Version"
x-backslash-rate-limit:
  defaultLimit: 60
  windowSeconds: 60
  headers:
    - "RateLimit-Limit"
    - "RateLimit-Remaining"
    - "RateLimit-Reset"
    - "Retry-After"
x-backslash-pagination:
  style: "cursor"
  responseFields:
    - "data"
    - "pagination.limit"
    - "pagination.total"
    - "pagination.hasMore"
    - "pagination.nextCursor"
    - "pagination.prevCursor"
  queryParams:
    preferred:
      - "cursor"
      - "limit"
    legacy:
      - "page"
      - "size"
  defaultLimit: 20
  maxLimit: 100
