openapi: 3.0.3
info:
  title: Dingdong Public Ordering API
  version: 1.0.0
  description: |
    Customer-facing integration contract for reading a public menu, creating an
    order, and reading that order's progress. This contract does not expose
    merchant back-office, POS, analytics, order-list, or order-status write APIs.

    POST /api/orders returns an order-specific accessToken. External callers must
    send that token back in X-Order-Access-Token when polling GET /api/orders/{orderId}
    or safely retrying the same POST /api/orders request with clientRequestId.
    Do not send this token in Authorization: Bearer. Legacy accessToken query and
    HttpOnly cookie flows remain supported for browser compatibility, but new
    integrations should use the header.
servers:
  - url: https://dingdongxi.com
    description: Production
  - url: http://localhost:3000
    description: Development
paths:
  /api/menu/{merchantIdentifier}:
    get:
      tags: [Public menu]
      summary: Read a merchant's public menu
      operationId: getPublicMenu
      security: []
      parameters:
        - name: merchantIdentifier
          in: path
          required: true
          description: Merchant identifier used by the public ordering flow.
          schema:
            type: string
        - name: table
          in: query
          required: false
          description: Optional table number. When supplied, it must be active for the merchant.
          schema:
            type: string
      responses:
        '200':
          description: Public merchant summary and available menu categories.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicMenuResponse'
        '400':
          description: The supplied table is not active for this merchant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Merchant not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/orders:
    post:
      tags: [Public orders]
      summary: Create an order or safely replay the same request
      operationId: createPublicOrder
      security: []
      description: |
        Prices and option adjustments in this request are validated and normalized
        by the server from the merchant's current catalog; clients must not treat
        submitted prices as authoritative. Set clientRequestId to a stable value
        for a retry-safe request. A replay for an anonymous order requires the
        original matching X-Order-Access-Token and is never authorized by
        clientRequestId alone.
      parameters:
        - $ref: '#/components/parameters/OrderAccessTokenHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePublicOrderRequest'
      responses:
        '201':
          description: Order created or an authorized idempotency replay returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateOrderResponse'
        '400':
          description: Request validation or server-side catalog/pricing validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: The clientRequestId belongs to an order the caller cannot access.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/orders/{orderId}:
    get:
      tags: [Public orders]
      summary: Read a created order's progress
      operationId: getPublicOrder
      security: []
      description: |
        External callers must send the accessToken returned by POST /api/orders in
        X-Order-Access-Token. Invalid, missing, expired, or mismatched tokens return
        the same generic 404 response as a nonexistent order. Existing member
        sessions and the legacy accessToken query/HttpOnly cookie flows continue to
        work for browser clients.
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/OrderAccessTokenHeader'
        - name: summary
          in: query
          required: false
          description: Set to '1' to omit order_items from the response.
          schema:
            type: string
            enum: ['1']
      responses:
        '200':
          description: Public order status and detail permitted by the order capability.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicOrderResponse'
        '404':
          description: Order not found or caller lacks the matching order capability.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  parameters:
    OrderAccessTokenHeader:
      name: X-Order-Access-Token
      in: header
      required: false
      description: |
        Per-order capability returned as accessToken by POST /api/orders. Required
        for external anonymous polling and anonymous idempotent retries. It is not
        an Authorization bearer token.
      schema:
        type: string
        minLength: 1
  schemas:
    CreatePublicOrderRequest:
      type: object
      required: [merchantId, tableId, tableNumber, items, totalAmount]
      properties:
        merchantId:
          type: string
          format: uuid
        tableId:
          type: string
          description: Public table identifier; walk-in and preview values are supported by the existing order flow.
        tableNumber:
          type: string
          maxLength: 20
        source:
          type: string
          enum: [qr, web]
          description: Omit to use the existing public-order default. POS is not part of this public contract.
        clientRequestId:
          type: string
          minLength: 8
          maxLength: 120
          description: Stable idempotency key scoped to the merchant.
        customerName:
          type: string
          maxLength: 100
        customerPhone:
          type: string
          description: Required by the existing validation rules for takeout/group-buy orders.
        note:
          type: string
          maxLength: 1000
        paymentMethod:
          type: string
          maxLength: 50
        couponId:
          type: string
          format: uuid
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/CreateOrderItem'
        totalAmount:
          type: number
          exclusiveMinimum: 0
          description: Client-provided total subject to server-side catalog and pricing validation.
    CreateOrderItem:
      type: object
      required: [menuItemId, menuItemName, quantity, unitPrice]
      properties:
        menuItemId:
          type: string
          format: uuid
        menuItemName:
          type: string
          maxLength: 100
        quantity:
          type: integer
          minimum: 1
          maximum: 99
        unitPrice:
          type: number
          exclusiveMinimum: 0
          description: Client-provided price subject to server-side validation.
        selectedOptions:
          description: Current catalog option selections. The server validates option availability and price adjustments.
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items:
                type: object
                additionalProperties: true
        note:
          type: string
          maxLength: 500
    CreateOrderResponse:
      type: object
      required: [data, idempotencyStatus]
      properties:
        data:
          type: object
          description: Created order payload from the existing order creation flow.
          additionalProperties: true
        accessToken:
          type: string
          description: Per-order capability for header-based polling and anonymous retries.
        idempotencyStatus:
          type: string
          enum: [created, replayed]
    PublicOrderResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/PublicOrder'
    PublicOrder:
      type: object
      required: [id, order_number, status, total_amount, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        order_number:
          type: string
        table_number:
          type: string
          nullable: true
        status:
          type: string
        total_amount:
          type: number
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        order_items:
          type: array
          description: Omitted when summary=1.
          items:
            $ref: '#/components/schemas/PublicOrderItem'
    PublicOrderItem:
      type: object
      properties:
        id:
          type: string
        menu_item_name:
          type: string
        quantity:
          type: integer
        unit_price:
          type: number
        selected_options:
          nullable: true
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items: {}
    PublicMenuResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [merchant, categories]
          properties:
            merchant:
              type: object
              required: [id, restaurantName]
              properties:
                id: { type: string }
                restaurantName: { type: string }
                logoUrl: { type: string, nullable: true }
            categories:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  name: { type: string }
                  sort_order: { type: integer }
                  menu_items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        description: { type: string, nullable: true }
                        price: { type: number }
                        original_price: { type: number }
                        sale_price: { type: number, nullable: true }
                        image_url: { type: string, nullable: true }
                        is_available: { type: boolean }
                        menu_item_options:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
        code:
          type: string
