openapi: 3.1.0
info:
  title: CheapEmail Open API
  version: 1.0.0
  description: |
    CheapEmail 站点对接 API。所有请求使用 API Key、Unix 时间戳和
    HMAC-SHA256 签名。签名路径不包含域名和查询参数。
  contact:
    url: https://cheapemail.cc/
servers:
  - url: https://cheapemail.cc/api/v1/upstream
    description: CheapEmail production
security:
  - ApiKey: []
    Timestamp: []
    Signature: []
tags:
  - name: Connection
  - name: Catalog
  - name: Orders
paths:
  /ping:
    post:
      tags: [Connection]
      summary: 测试连通性和账户状态
      operationId: ping
      responses:
        "200":
          description: 连接成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PingResponse"
        "401":
          $ref: "#/components/responses/AuthError"
        "403":
          $ref: "#/components/responses/AuthError"
  /categories:
    get:
      tags: [Catalog]
      summary: 获取分类列表
      operationId: listCategories
      responses:
        "200":
          description: 分类列表
          content:
            application/json:
              schema:
                type: object
                required: [ok, categories]
                properties:
                  ok:
                    const: true
                  categories:
                    type: array
                    items:
                      $ref: "#/components/schemas/Category"
        "401":
          $ref: "#/components/responses/AuthError"
  /products:
    get:
      tags: [Catalog]
      summary: 分页获取商品与 SKU
      operationId: listProducts
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: 商品分页
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductsResponse"
        "401":
          $ref: "#/components/responses/AuthError"
  /products/{id}:
    get:
      tags: [Catalog]
      summary: 获取商品详情
      operationId: getProduct
      parameters:
        - $ref: "#/components/parameters/ProductId"
      responses:
        "200":
          description: 商品详情
          content:
            application/json:
              schema:
                type: object
                required: [ok, product]
                properties:
                  ok:
                    const: true
                  product:
                    $ref: "#/components/schemas/Product"
        "404":
          $ref: "#/components/responses/BusinessError"
  /orders:
    post:
      tags: [Orders]
      summary: 创建采购订单
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "200":
          description: 创建结果；支付失败时也可能返回 200 和 ok=false
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CreateOrderResponse"
                  - $ref: "#/components/schemas/Error"
        "400":
          $ref: "#/components/responses/BusinessError"
        "402":
          $ref: "#/components/responses/BusinessError"
        "409":
          $ref: "#/components/responses/BusinessError"
  /orders/{id}:
    get:
      tags: [Orders]
      summary: 查询订单和交付数据
      operationId: getOrder
      parameters:
        - $ref: "#/components/parameters/OrderId"
      responses:
        "200":
          description: 订单详情
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderDetail"
        "404":
          $ref: "#/components/responses/BusinessError"
  /orders/{id}/cancel:
    post:
      tags: [Orders]
      summary: 取消订单
      operationId: cancelOrder
      parameters:
        - $ref: "#/components/parameters/OrderId"
      responses:
        "200":
          description: 取消成功
          content:
            application/json:
              schema:
                type: object
                required: [ok, order_id, order_no, status]
                properties:
                  ok:
                    const: true
                  order_id:
                    type: integer
                  order_no:
                    type: string
                  status:
                    const: canceled
        "404":
          $ref: "#/components/responses/BusinessError"
        "409":
          $ref: "#/components/responses/BusinessError"
webhooks:
  orderStatus:
    post:
      summary: 订单状态与交付回调
      security:
        - ApiKey: []
          Timestamp: []
          Signature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderCallback"
      responses:
        "200":
          description: 回调已接收
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                  message:
                    type: string
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: Dujiao-Next-Api-Key
      description: 对接用户的 API Key
    Timestamp:
      type: apiKey
      in: header
      name: Dujiao-Next-Timestamp
      description: Unix 秒级时间戳，允许约 ±60 秒偏差
    Signature:
      type: apiKey
      in: header
      name: Dujiao-Next-Signature
      description: HMAC-SHA256 小写十六进制签名
  parameters:
    ProductId:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    OrderId:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
  responses:
    AuthError:
      description: 鉴权失败
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BusinessError:
      description: 业务失败
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    MultiLanguageText:
      type: object
      additionalProperties:
        type: string
      examples:
        - zh-CN: 示例商品
          en-US: Example Product
    Error:
      type: object
      required: [ok, error_code, error_message]
      properties:
        ok:
          const: false
        error_code:
          type: string
        error_message:
          type: string
    PingResponse:
      type: object
      required: [ok, site_name, protocol_version, user_id, balance, currency]
      properties:
        ok:
          const: true
        site_name:
          type: string
        protocol_version:
          const: "1.0"
        user_id:
          type: integer
        balance:
          type: string
        currency:
          type: string
        member_level:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/MemberLevel"
    MemberLevel:
      type: object
      properties:
        id:
          type: integer
        name:
          $ref: "#/components/schemas/MultiLanguageText"
        slug:
          type: string
        icon:
          type: string
          format: uri
    Category:
      type: object
      required: [id, parent_id, slug, name, sort_order]
      properties:
        id:
          type: integer
        parent_id:
          type: integer
        slug:
          type: string
        name:
          $ref: "#/components/schemas/MultiLanguageText"
        icon:
          type: string
        sort_order:
          type: integer
    SKU:
      type: object
      required:
        - id
        - sku_code
        - spec_values
        - price_amount
        - stock_status
        - stock_quantity
        - is_active
      properties:
        id:
          type: integer
          description: 创建订单时作为 sku_id 提交
        sku_code:
          type: string
        spec_values:
          type: object
          additionalProperties: true
        price_amount:
          type: string
        original_price:
          type: string
        member_price:
          type: string
        stock_status:
          type: string
          enum: [unlimited, in_stock, low_stock, out_of_stock]
        stock_quantity:
          type: integer
          description: -1 表示无限库存
        is_active:
          type: boolean
    Product:
      type: object
      required:
        - id
        - slug
        - title
        - description
        - price_amount
        - fulfillment_type
        - is_active
        - category_id
        - skus
      properties:
        id:
          type: integer
        slug:
          type: string
        title:
          $ref: "#/components/schemas/MultiLanguageText"
        description:
          $ref: "#/components/schemas/MultiLanguageText"
        content:
          $ref: "#/components/schemas/MultiLanguageText"
        seo_meta:
          type: object
          additionalProperties: true
        images:
          type: array
          items:
            type: string
        tags:
          type: array
          items:
            type: string
        price_amount:
          type: string
        original_price:
          type: string
        member_price:
          type: string
        fulfillment_type:
          type: string
          enum: [auto, manual]
        manual_form_schema:
          type: [object, "null"]
          additionalProperties: true
        is_active:
          type: boolean
        category_id:
          type: integer
        skus:
          type: array
          items:
            $ref: "#/components/schemas/SKU"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ProductsResponse:
      type: object
      required: [ok, items, total, page, page_size]
      properties:
        ok:
          const: true
        items:
          type: array
          items:
            $ref: "#/components/schemas/Product"
        total:
          type: integer
        page:
          type: integer
        page_size:
          type: integer
    CreateOrderRequest:
      type: object
      description: Recommended request body contains only sku_id and quantity. Client-facing tools resolve sku_code to sku_id before sending this request.
      required: [sku_id, quantity]
      properties:
        sku_id:
          type: integer
          minimum: 1
        quantity:
          type: integer
          minimum: 1
        manual_form_data:
          type: object
          additionalProperties: true
        downstream_order_no:
          type: string
          description: 可选的下游订单号
        trace_id:
          type: string
        callback_url:
          type: string
          format: uri
    CreateOrderResponse:
      type: object
      required: [ok, order_id, order_no, status, amount, currency]
      properties:
        ok:
          const: true
        order_id:
          type: integer
        order_no:
          type: string
        status:
          type: string
        amount:
          type: string
        currency:
          type: string
    OrderItem:
      type: object
      properties:
        product_id:
          type: integer
        sku_id:
          type: integer
        title:
          $ref: "#/components/schemas/MultiLanguageText"
        quantity:
          type: integer
        unit_price:
          type: string
        total_price:
          type: string
        fulfillment_type:
          type: string
          enum: [auto, manual]
    Fulfillment:
      type: object
      required: [type, status, payload, delivered_at]
      properties:
        type:
          type: string
          enum: [auto, manual]
        status:
          type: string
          enum: [delivered]
        payload:
          type: string
          description: Purchased data separated by newlines. Each non-empty line is one purchased item.
          examples:
            - |-
              data-1
              data-2
        delivery_data:
          type: [object, "null"]
          additionalProperties: true
        delivered_at:
          type: string
          format: date-time
    OrderDetail:
      type: object
      required: [ok, order_id, order_no, status, amount, currency, items]
      properties:
        ok:
          const: true
        order_id:
          type: integer
        order_no:
          type: string
        status:
          $ref: "#/components/schemas/OrderStatus"
        amount:
          type: string
        currency:
          type: string
        items:
          type: array
          items:
            $ref: "#/components/schemas/OrderItem"
        fulfillment:
          $ref: "#/components/schemas/Fulfillment"
    OrderStatus:
      type: string
      enum:
        - pending_payment
        - paid
        - fulfilling
        - partially_delivered
        - delivered
        - completed
        - canceled
    OrderCallback:
      type: object
      required: [order_id, status]
      properties:
        event:
          type: string
        order_id:
          type: integer
        order_no:
          type: string
        downstream_order_no:
          type: string
        status:
          $ref: "#/components/schemas/OrderStatus"
        fulfillment:
          $ref: "#/components/schemas/Fulfillment"
        timestamp:
          type: integer
