{
  "openapi": "3.0.3",
  "info": {
    "title": "istatus Open API",
    "version": "1.0.0",
    "description": "API สำหรับให้ ERP / โปรแกรมบัญชี / POS ของร้านดึงข้อมูล ออเดอร์ · สินค้า · สต๊อก · ลูกค้า จาก istatus (รวมทุกช่องทาง Shopee/Lazada/TikTok/POS ด้วย key เดียว)\n\n**การยืนยันตัวตน:** ส่ง header `Authorization: Bearer <API key>` ทุกคำขอ — ออก key ได้ที่หน้า ตั้งค่า → เชื่อมต่อ API / ERP (เจ้าของร้านเท่านั้น)\n\n**สิทธิ์:** แพ็ก Pro ขึ้นไปใช้ได้ตลอด · แพ็ก Free/Starter ทดลองฟรี 30 วันนับจากออก key ใบแรก (ช่วงทดลองทุก response มี header `X-Trial-Days-Left`)\n\n**หน่วยเงิน:** ทุกช่องจำนวนเงินเป็น **สตางค์ (integer)** — 100 = 1 บาท · `vatRate` เป็น per-10000 (700 = 7.00%)\n\n**ความถี่:** อ่าน ~10 ครั้ง/วินาที/ร้าน · เกิน = 429 พร้อม header `Retry-After` (วินาที) — รอแล้วยิงใหม่\n\n**Pagination:** ทุก list ใช้ cursor: อ่าน `nextCursor` จาก response แล้วส่งกลับเป็นพารามิเตอร์ `cursor` จนกว่าจะได้ `null` · `limit` สูงสุด 100\n\nคู่มือฉบับเต็ม + ตัวอย่างโค้ด 5 ภาษา: https://istatus.net/developers",
    "contact": {
      "name": "istatus",
      "url": "https://istatus.net/developers"
    }
  },
  "servers": [
    {
      "url": "https://istatus.net",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "system",
      "description": "เช็คสถานะ key / แพ็ก"
    },
    {
      "name": "orders",
      "description": "ออเดอร์รวมทุกช่องทาง"
    },
    {
      "name": "products",
      "description": "สินค้า + ราคาขาย"
    },
    {
      "name": "stock",
      "description": "ยอดสต๊อกคงเหลือ"
    },
    {
      "name": "customers",
      "description": "ลูกค้าของร้าน"
    }
  ],
  "paths": {
    "/api/v1/ping": {
      "get": {
        "tags": [
          "system"
        ],
        "summary": "เช็ค key + แพ็ก + วันทดลองคงเหลือ",
        "description": "ใช้ทดสอบว่า key ใช้งานได้ — ตอบชื่อร้าน แพ็กปัจจุบัน และจำนวนวันทดลองที่เหลือ (null = แพ็กจ่ายจริง ไม่ใช่โหมดทดลอง)",
        "operationId": "ping",
        "responses": {
          "200": {
            "description": "key ใช้งานได้",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "plan": {
                      "type": "string",
                      "nullable": true,
                      "description": "แพ็กปัจจุบัน: free | starter | pro | business | scale",
                      "example": "pro"
                    },
                    "trialDaysLeft": {
                      "type": "integer",
                      "nullable": true,
                      "description": "วันทดลองที่เหลือ — null = แพ็ก Pro ขึ้นไป (ไม่ใช่โหมดทดลอง)",
                      "example": null
                    },
                    "shopName": {
                      "type": "string",
                      "nullable": true,
                      "example": "ร้านตัวอย่าง"
                    },
                    "heldCount": {
                      "type": "integer",
                      "nullable": true,
                      "description": "จำนวนออเดอร์ push ที่ค้างห้องรอ (โควตาเต็ม) — null = ร้านยังไม่ได้เปิดใช้ฝั่งเขียน",
                      "example": 0
                    },
                    "rejectedCount": {
                      "type": "integer",
                      "nullable": true,
                      "description": "จำนวนออเดอร์ push ที่ระบบลองปล่อยจากห้องรอแล้วไม่ผ่านจนครบเพดาน (rejected) — **ยังไม่เป็นยอดขาย** ดูสาเหตุที่ GET /api/v1/orders/held (rejectedRows) แล้วส่ง externalOrderNo เดิมมาใหม่ · null = ร้านยังไม่ได้เปิดใช้ฝั่งเขียน",
                      "example": 0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/orders": {
      "get": {
        "tags": [
          "orders"
        ],
        "summary": "รายการออเดอร์ (รวมทุกช่อง + รายการสินค้าในบิล)",
        "description": "เรียงจากใหม่ไปเก่า (created_at DESC) · แต่ละออเดอร์แนบ `items[]` มาให้ครบในคำขอเดียว\n\n**ข้อจำกัดข้อมูล marketplace:** เมื่อกรอง `channel` เป็น shopee/lazada/tiktok/woocommerce ระบบจะจำกัดข้อมูลย้อนหลัง 90 วันตามนโยบายคุ้มครองข้อมูลของแพลตฟอร์ม (response จะมี `mpLimited: true` และ `retentionDays: 90`)",
        "operationId": "listOrders",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "วันที่เริ่ม (ตามเขตเวลาระบบ) รูปแบบ YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-08-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "วันที่สิ้นสุด (รวมทั้งวัน) รูปแบบ YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-08-31"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "description": "กรองช่องทาง เช่น pos, shopee, lazada, tiktok, woocommerce, web · ใช้ api = เฉพาะออเดอร์ที่ระบบของคุณส่งเข้ามาผ่าน POST /orders (กันดึงกลับไปซ้ำ)",
            "schema": {
              "type": "string",
              "example": "shopee"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "กรองสถานะ เช่น paid, voided",
            "schema": {
              "type": "string",
              "example": "paid"
            }
          },
          {
            "$ref": "#/components/parameters/cursor"
          },
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "หน้าของรายการออเดอร์",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "ส่งกลับเป็น ?cursor= เพื่อดึงหน้าถัดไป · null = หมดแล้ว"
                    },
                    "mpLimited": {
                      "type": "boolean",
                      "description": "true = คำขอนี้ถูกจำกัดย้อนหลัง 90 วัน (กรอง channel marketplace)"
                    },
                    "retentionDays": {
                      "type": "integer",
                      "nullable": true,
                      "example": null
                    }
                  }
                },
                "example": {
                  "rows": [
                    {
                      "id": "0d3f2a10-1111-4222-8333-944455556666",
                      "orderNo": "POS-1756600000000-123",
                      "channel": "pos",
                      "origin": "pos",
                      "status": "paid",
                      "priceType": "retail",
                      "total": 25000,
                      "vat": 1636,
                      "vatRate": 700,
                      "vatMode": "inclusive",
                      "discount": 0,
                      "shippingFee": 0,
                      "shippingMethod": null,
                      "currency": "THB",
                      "customerId": null,
                      "trackingNo": null,
                      "carrier": null,
                      "note": null,
                      "pointsEarned": 10,
                      "pointsUsed": 0,
                      "fulfillmentStage": null,
                      "issuedAt": null,
                      "shippedAt": null,
                      "deliveredAt": null,
                      "createdAt": "2026-08-31T04:10:22.000Z",
                      "items": [
                        {
                          "productId": "7b1c0000-2222-4333-8444-955566667777",
                          "sku": "A-001",
                          "name": "สินค้าตัวอย่าง",
                          "qty": 2,
                          "price": 12500
                        }
                      ]
                    }
                  ],
                  "nextCursor": null,
                  "mpLimited": false,
                  "retentionDays": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "orders"
        ],
        "summary": "ส่งออเดอร์จากระบบของคุณเข้า istatus (push)",
        "description": "บันทึกออเดอร์ผ่านเส้นทางเดียวกับหน้าขายของ istatus (ตัดสต๊อก + ลงบัญชีอัตโนมัติ) — ราคาต่อชิ้นมาจากราคาขายในแคตตาล็อกของร้าน (ตามระดับราคาลูกค้า ถ้าระบุ customerId)\n\n**Idempotency:** ยิงซ้ำ `externalOrderNo` เดิม = ได้ผลเดิมกลับ (200) ไม่บันทึกซ้ำ\n\n**โควตาออเดอร์ของแพ็กเต็ม = 202 ไม่ใช่ error:** ออเดอร์เข้าห้องรอ (held) และถูกปล่อยเข้าอัตโนมัติเมื่ออัปเกรดแพ็ก/ขึ้นเดือนใหม่ — **ไม่ต้องยิงซ้ำ**\n\n**ห้องรอมีเพดาน 5,000 ใบ/ร้าน:** เกินแล้วใบใหม่ได้ 409 `HELD_FULL` (ของที่รับไว้แล้วไม่หาย) — อัปเกรดแพ็กเพื่อปล่อยคิวแล้วส่งใหม่\n\nความถี่ฝั่งเขียน ~2 ครั้ง/วินาที/ร้าน · ร้านที่ยังไม่ได้เปิดใช้ฝั่งเขียนจะได้ 404",
        "operationId": "createOrder",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "externalOrderNo",
                  "items"
                ],
                "properties": {
                  "externalOrderNo": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "เลขออเดอร์ในระบบของคุณ — ใช้กันบันทึกซ้ำ (idempotency) · เลขบิลใน istatus = API-<เลขนี้>",
                    "example": "PO-2026-0042"
                  },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "sku",
                        "qty"
                      ],
                      "properties": {
                        "sku": {
                          "type": "string",
                          "maxLength": 120,
                          "example": "A-001"
                        },
                        "qty": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 1000000,
                          "example": 2
                        }
                      }
                    },
                    "description": "รายการสินค้า (SKU ต้องมีอยู่ในร้าน) — ราคาใช้ราคาขายในแคตตาล็อก ไม่รับราคาจากภายนอก"
                  },
                  "customerId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ลูกค้าในร้าน (id จาก GET /api/v1/customers) — ใช้ระดับราคาของลูกค้ารายนั้น"
                  },
                  "shipping": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "ค่าขนส่งที่คิดกับลูกค้า (สตางค์)"
                  },
                  "shippingMethod": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "issuedAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "วันที่เอกสาร (ISO 8601) — ใช้แสดงบนเอกสารเท่านั้น"
                  },
                  "trackingNo": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "carrier": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "recipientName": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "recipientPhone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "recipientProvince": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "recipientPostal": {
                    "type": "string",
                    "maxLength": 20
                  }
                }
              },
              "example": {
                "externalOrderNo": "PO-2026-0042",
                "items": [
                  {
                    "sku": "A-001",
                    "qty": 2
                  }
                ],
                "note": "ส่งด่วน"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "บันทึกแล้ว",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "created"
                    },
                    "orderNo": {
                      "type": "string",
                      "example": "API-PO-2026-0042"
                    }
                  }
                },
                "example": {
                  "status": "created",
                  "orderNo": "API-PO-2026-0042"
                }
              }
            }
          },
          "202": {
            "description": "โควตาออเดอร์เดือนนี้เต็ม — รับเข้าห้องรอแล้ว (ไม่ใช่ error ไม่ต้องยิงซ้ำ)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "held"
                    },
                    "heldCount": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                },
                "example": {
                  "status": "held",
                  "heldCount": 3
                }
              }
            }
          },
          "200": {
            "description": "ยิงซ้ำ externalOrderNo เดิม — คืนผลเดิม ไม่บันทึกซ้ำ · ถ้าบิลเลขนี้ถูกยกเลิก (void) ในร้านไปแล้ว จะได้ {\"status\":\"voided\"} และไม่บันทึกใหม่ — ต้องการบันทึกใหม่จริงให้ใช้ externalOrderNo ใหม่",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "example": "created",
                          "enum": [
                            "created",
                            "held",
                            "voided"
                          ]
                        },
                        "orderNo": {
                          "type": "string",
                          "example": "API-PO-2026-0042"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "example": "held",
                          "enum": [
                            "created",
                            "held",
                            "voided"
                          ]
                        },
                        "heldCount": {
                          "type": "integer",
                          "example": 3
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "409": {
            "description": "code=INSUFFICIENT_STOCK สต๊อกไม่พอ (เติมสต๊อกแล้วยิงใหม่) · code=HELD_FULL ห้องรอของร้านเต็ม (ใบนี้ยังไม่ถูกรับ — อัปเกรดแพ็กเพื่อปล่อยคิว แล้วส่ง externalOrderNo เดิมมาใหม่) · code=IN_PROGRESS เลขนี้กำลังถูกบันทึกโดยคำขออื่นพร้อมกัน — รอ 2 วินาทีแล้วส่งเลขเดิมซ้ำ (จะได้ created/held/voided ตามจริง)",
            "content": {
              "application/json": {
                "examples": {
                  "insufficientStock": {
                    "value": {
                      "code": "INSUFFICIENT_STOCK",
                      "message": "สต๊อกไม่พอสำหรับ SKU \"A-001\" — เติมสต๊อกแล้วยิงใหม่"
                    }
                  },
                  "heldFull": {
                    "value": {
                      "code": "HELD_FULL",
                      "message": "ห้องรอของร้านเต็ม (5,000 ใบ) — โควตาออเดอร์เดือนนี้เต็มและมีออเดอร์ค้างรอเข้าถึงเพดานแล้ว ใบนี้ยังไม่ถูกรับ: อัปเกรดแพ็กเพื่อปล่อยคิวก่อน แล้วส่ง externalOrderNo เดิมมาใหม่"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/products": {
      "get": {
        "tags": [
          "products"
        ],
        "summary": "รายการสินค้า + ราคาขาย",
        "description": "เรียงตาม SKU (A→Z) · คืนทั้งสินค้าที่เปิดและปิดใช้งาน (`active`) เพื่อให้ระบบที่ sync เห็นตัวที่ถูกปิดด้วย · ไม่มีข้อมูลต้นทุน",
        "operationId": "listProducts",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "ค้นหาจาก SKU / ชื่อสินค้า (บางส่วน) หรือบาร์โค้ด (ตรงตัว)",
            "schema": {
              "type": "string",
              "example": "A-0"
            }
          },
          {
            "$ref": "#/components/parameters/cursor"
          },
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "หน้าของรายการสินค้า",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "rows": [
                    {
                      "id": "7b1c0000-2222-4333-8444-955566667777",
                      "sku": "A-001",
                      "name": "สินค้าตัวอย่าง",
                      "category": "ทั่วไป",
                      "barcode": "8850000000001",
                      "price": 12500,
                      "priceWholesale": 11000,
                      "priceVip": null,
                      "stock": 42,
                      "weightG": 500,
                      "dimensions": "20x15x10",
                      "description": null,
                      "imageUrl": "/uploads/img-xxxx.jpg",
                      "active": true
                    }
                  ],
                  "nextCursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/stock": {
      "get": {
        "tags": [
          "stock"
        ],
        "summary": "ยอดสต๊อกคงเหลือ",
        "description": "ยอดรวมต่อสินค้า (ค่าเดียวกับหน้าเว็บ istatus) · เฉพาะสินค้าที่เปิดใช้งาน · เรียงตาม SKU · ไม่มีมูลค่า/ต้นทุน",
        "operationId": "listStock",
        "parameters": [
          {
            "name": "sku",
            "in": "query",
            "description": "ดูเฉพาะ SKU นี้ (ตรงตัว)",
            "schema": {
              "type": "string",
              "example": "A-001"
            }
          },
          {
            "$ref": "#/components/parameters/cursor"
          },
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "หน้าของยอดสต๊อก",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StockItem"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "rows": [
                    {
                      "productId": "7b1c0000-2222-4333-8444-955566667777",
                      "sku": "A-001",
                      "name": "สินค้าตัวอย่าง",
                      "stock": 42
                    }
                  ],
                  "nextCursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/customers": {
      "get": {
        "tags": [
          "customers"
        ],
        "summary": "รายการลูกค้าของร้าน",
        "description": "ลูกค้าที่ร้านบันทึกเอง (ร้านเป็นเจ้าของข้อมูล) · เรียงจากใหม่ไปเก่า — ไม่รวมผู้ซื้อฝั่ง marketplace",
        "operationId": "listCustomers",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "ค้นหาจากชื่อหรือเบอร์โทร (บางส่วน)",
            "schema": {
              "type": "string",
              "example": "สมชาย"
            }
          },
          {
            "$ref": "#/components/parameters/cursor"
          },
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "หน้าของรายการลูกค้า",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Customer"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "rows": [
                    {
                      "id": "5e9a0000-3333-4444-8555-966677778888",
                      "name": "สมชาย ใจดี",
                      "phone": "0812345678",
                      "taxId": null,
                      "address": "99/1 ถ.ตัวอย่าง ต.ในเมือง อ.เมือง จ.ขอนแก่น 40000",
                      "branch": null,
                      "contact": null,
                      "points": 120,
                      "note": null,
                      "shippingMethod": null,
                      "priceTierId": null,
                      "createdAt": "2026-07-15T09:30:00.000Z"
                    }
                  ],
                  "nextCursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/orders/held": {
      "get": {
        "tags": [
          "orders"
        ],
        "summary": "ดูคิวออเดอร์ที่ค้างห้องรอ (โควตาเต็ม)",
        "description": "ออเดอร์ push ที่ยังไม่ถูกบันทึก — `rows` = ค้างห้องรอ (โควตาเต็ม หรือรอเติมสต๊อก) เรียงตามลำดับที่จะถูกปล่อยเข้าอัตโนมัติ (100 รายการแรก) · `rejectedRows` = ระบบลองปล่อยแล้วไม่ผ่านจนครบเพดาน (เช่น SKU ไม่มีในร้าน) **ยังไม่เป็นยอดขาย** — แก้ตาม `lastError` แล้วส่ง `externalOrderNo` เดิมมาใหม่ (ใหม่สุดก่อน · 100 รายการ)\n\n**quarantined:** ใบที่ส่งมาด้วย key ที่ถูกเพิกถอนภายหลัง จะถูกกักไว้ (ไม่บันทึก ไม่ลบ) — ส่ง externalOrderNo เดิมด้วย key ที่ใช้งานได้ ระบบจะโอนใบกลับเข้าคิวให้",
        "operationId": "listHeldOrders",
        "responses": {
          "200": {
            "description": "คิวห้องรอของร้าน",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "orderNo": {
                            "type": "string",
                            "example": "API-PO-2026-0042"
                          },
                          "attemptCount": {
                            "type": "integer",
                            "description": "จำนวนครั้งที่ระบบลองปล่อยแล้วไม่ผ่าน (เช่น สต๊อกไม่พอ) — โควตาเต็มไม่นับ"
                          },
                          "lastError": {
                            "type": "string",
                            "nullable": true,
                            "description": "สาเหตุที่ปล่อยไม่ผ่านครั้งล่าสุด — `insufficient stock: <sku>` = รอเติมสต๊อก (ระบบลองใหม่เองทุก ~5 นาที ไม่นับเป็นความพยายาม)"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "held",
                              "rejected"
                            ]
                          }
                        }
                      }
                    },
                    "heldCount": {
                      "type": "integer"
                    },
                    "heldStockCount": {
                      "type": "integer",
                      "description": "ในจำนวน heldCount นี้ มีกี่ใบที่ติดสต๊อกไม่พอ (รอเติมของ)"
                    },
                    "rejectedRows": {
                      "type": "array",
                      "description": "ใบที่ปล่อยไม่ผ่านจนครบเพดาน — ยังไม่เป็นยอดขาย ต้องแก้แล้วส่งใหม่",
                      "items": {
                        "$ref": "#/paths/~1api~1v1~1orders~1held/get/responses/200/content/application~1json/schema/properties/rows/items"
                      }
                    },
                    "rejectedCount": {
                      "type": "integer"
                    },
                    "quarantinedRows": {
                      "type": "array",
                      "description": "ใบที่ถูกกักเพราะ API key ที่ส่งมาถูกเพิกถอน — ไม่ถูกบันทึก ไม่ถูกลบ · ส่ง externalOrderNo เดิมด้วย key ที่ใช้งานได้เพื่อโอนกลับเข้าคิว",
                      "items": {
                        "type": "object"
                      }
                    },
                    "quarantinedCount": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "rows": [
                    {
                      "id": "7b1c0000-2222-4333-8444-955566667777",
                      "orderNo": "API-PO-2026-0042",
                      "status": "held",
                      "attemptCount": 0,
                      "lastError": null,
                      "createdAt": "2026-08-31T10:00:00+07:00"
                    }
                  ],
                  "heldCount": 1,
                  "heldStockCount": 0,
                  "rejectedRows": [
                    {
                      "id": "7b1c0000-2222-4333-8444-955566668888",
                      "orderNo": "API-PO-2026-0039",
                      "status": "rejected",
                      "attemptCount": 5,
                      "lastError": "product not found/inactive: OLD-SKU",
                      "createdAt": "2026-08-30T09:00:00+07:00"
                    }
                  ],
                  "rejectedCount": 1,
                  "quarantinedRows": [],
                  "quarantinedCount": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/stock/{sku}": {
      "put": {
        "tags": [
          "stock"
        ],
        "summary": "ตั้งยอดสต๊อกคงเหลือ (absolute)",
        "description": "ตั้งยอดคงเหลือใหม่ให้ SKU นี้ — ระบบสร้างใบปรับสต๊อก (movement + บัญชี) ให้อัตโนมัติเหมือนกดปรับจากหน้าเว็บ · ยอดตรงอยู่แล้ว = ไม่สร้างใบเปล่า\n\nข้อจำกัด: สินค้าจัดชุด (bundle) ปรับตรงไม่ได้ — ยอดของชุดคำนวณจากสินค้าฐาน (422 code=BUNDLE_SKU) · ร้านที่ยังไม่ได้เปิดใช้ฝั่งเขียนจะได้ 404",
        "operationId": "setStock",
        "parameters": [
          {
            "name": "sku",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "A-001"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "qty"
                ],
                "properties": {
                  "qty": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "ยอดคงเหลือใหม่ (จำนวนเต็ม ≥ 0)",
                    "example": 42
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "หมายเหตุบนใบปรับสต๊อก"
                  },
                  "expectedQty": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "(แนะนำ) ยอดที่คุณอ่านไปล่าสุดจาก GET /stock — ถ้ายอดจริงตอนนี้ไม่ตรง (หน้าร้านขายไประหว่างนั้น) จะได้ 409 STALE_STOCK แทนการทับยอดด้วยข้อมูลเก่า"
                  }
                }
              },
              "example": {
                "qty": 42,
                "expectedQty": 44,
                "note": "sync จาก ERP"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ตั้งยอดแล้ว",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "sku": {
                      "type": "string"
                    },
                    "stock": {
                      "type": "integer",
                      "description": "ยอดคงเหลือหลังปรับ"
                    },
                    "delta": {
                      "type": "integer",
                      "description": "ผลต่างที่ปรับ (+เพิ่ม / -ลด / 0 = ยอดตรงอยู่แล้ว)"
                    },
                    "adjusted": {
                      "type": "boolean"
                    },
                    "adjustNo": {
                      "type": "string",
                      "description": "เลขใบปรับสต๊อก (เฉพาะตอนมีการปรับจริง)"
                    }
                  }
                },
                "example": {
                  "status": "ok",
                  "sku": "A-001",
                  "stock": 42,
                  "delta": 5,
                  "adjusted": true,
                  "adjustNo": "API-ADJ-1756600000000-123"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "404": {
            "description": "ไม่พบ SKU นี้ในร้าน (หรือถูกปิดใช้งาน)",
            "content": {
              "application/json": {
                "example": {
                  "code": "NOT_FOUND",
                  "message": "ไม่พบสินค้า SKU \"A-001\" ในร้าน (หรือถูกปิดใช้งาน)"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "code=RETRY เลขใบปรับสต๊อกชนกัน (พบยากมาก) — ยอดยังไม่ถูกแตะ ลองใหม่อีกครั้ง",
            "content": {
              "application/json": {
                "example": {
                  "code": "RETRY",
                  "message": "เลขใบปรับสต๊อกชนกัน — ลองใหม่อีกครั้ง (ยอดสต๊อกยังไม่ถูกแตะ)"
                }
              }
            }
          },
          "409": {
            "description": "code=STALE_STOCK ยอดจริงไม่ตรง expectedQty (มีการเปลี่ยนแปลงระหว่างนั้น) — อ่านยอดใหม่แล้วส่งอีกครั้ง · ไม่มีอะไรถูกแตะ",
            "content": {
              "application/json": {
                "example": {
                  "code": "STALE_STOCK",
                  "message": "ยอดปัจจุบันของ \"SKU-1\" = 8 ไม่ตรง expectedQty 10 — มีการเปลี่ยนแปลงระหว่างนั้น อ่านยอดใหม่ (GET /stock) แล้วส่งอีกครั้ง"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key ของร้าน (ขึ้นต้น ist_) — ออกที่หน้า ตั้งค่า → เชื่อมต่อ API / ERP · เก็บใน env เท่านั้น ห้าม hardcode"
      }
    },
    "parameters": {
      "cursor": {
        "name": "cursor",
        "in": "query",
        "description": "ตัวชี้หน้าถัดไป — ใช้ค่า `nextCursor` จาก response ก่อนหน้าเท่านั้น (opaque token แก้เองไม่ได้)",
        "schema": {
          "type": "string"
        }
      },
      "limit": {
        "name": "limit",
        "in": "query",
        "description": "จำนวนต่อหน้า (default 50, สูงสุด 100)",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "key ไม่ถูกต้อง / ถูกเพิกถอน / ไม่ได้ส่ง Authorization",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "INVALID_KEY",
              "message": "API key ไม่ถูกต้องหรือถูกเพิกถอนแล้ว"
            }
          }
        }
      },
      "PlanRequired": {
        "description": "แพ็กไม่ถึงเกณฑ์ (PLAN_REQUIRED) หรือช่วงทดลอง 30 วันหมด (TRIAL_EXPIRED)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "TRIAL_EXPIRED",
              "message": "ช่วงทดลองใช้ API ฟรี 30 วันหมดแล้ว — อัปเกรดเป็นแพ็ก Pro ขึ้นไปเพื่อใช้งานต่อ"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "พารามิเตอร์/cursor ไม่ถูกต้อง (INVALID_PARAM | INVALID_CURSOR)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "INVALID_CURSOR",
              "message": "cursor ไม่ถูกต้อง — ใช้ค่า nextCursor จาก response ก่อนหน้าเท่านั้น"
            }
          }
        }
      },
      "RateLimited": {
        "description": "ยิงถี่เกินฟิวส์ต่อร้าน — ดู header `Retry-After` (วินาที) แล้วลองใหม่",
        "headers": {
          "Retry-After": {
            "description": "จำนวนวินาทีที่ควรรอ",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "RATE_LIMITED",
              "message": "ยิงถี่เกินกำหนด — ดู header Retry-After แล้วลองใหม่"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "รูปแบบ error เดียวกันทุก endpoint — `code` สำหรับเครื่องอ่าน · `message` ภาษาไทยสำหรับคน",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "INVALID_KEY",
              "PLAN_REQUIRED",
              "TRIAL_EXPIRED",
              "RATE_LIMITED",
              "INVALID_PARAM",
              "INVALID_CURSOR",
              "INTERNAL"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "message"
        ]
      },
      "OrderItem": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "sku": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "ชื่อสินค้า ณ ปัจจุบัน (null = สินค้าถูกลบ/ไม่ผูก)"
          },
          "qty": {
            "type": "integer"
          },
          "price": {
            "type": "integer",
            "description": "ราคาต่อหน่วย (สตางค์)"
          }
        }
      },
      "Order": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "orderNo": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "description": "pos | shopee | lazada | tiktok | woocommerce | web ฯลฯ"
          },
          "origin": {
            "type": "string",
            "description": "ที่มาจริงของออเดอร์: api = ระบบของคุณส่งเข้าผ่าน POST /orders (channel จะเป็น pos) · อื่น ๆ = ค่าเดียวกับ channel — ใช้ origin แยกออเดอร์ที่ ERP ส่งเองออกจากขายหน้าร้านจริง"
          },
          "status": {
            "type": "string",
            "description": "เช่น paid (ขายแล้ว), voided (ยกเลิกบิล)"
          },
          "priceType": {
            "type": "string",
            "description": "retail | wholesale | vip"
          },
          "total": {
            "type": "integer",
            "description": "ยอดที่ลูกค้าจ่ายจริง รวม VAT (สตางค์)"
          },
          "vat": {
            "type": "integer",
            "description": "VAT ในบิล (สตางค์)"
          },
          "vatRate": {
            "type": "integer",
            "description": "อัตรา VAT per-10000 (700 = 7.00%)"
          },
          "vatMode": {
            "type": "string",
            "description": "inclusive | exclusive"
          },
          "discount": {
            "type": "integer",
            "description": "ส่วนลดท้ายบิล (สตางค์)"
          },
          "shippingFee": {
            "type": "integer",
            "description": "ค่าขนส่งที่คิดกับลูกค้า (สตางค์)"
          },
          "shippingMethod": {
            "type": "string",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "example": "THB"
          },
          "customerId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "trackingNo": {
            "type": "string",
            "nullable": true
          },
          "carrier": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "pointsEarned": {
            "type": "integer"
          },
          "pointsUsed": {
            "type": "integer"
          },
          "fulfillmentStage": {
            "type": "string",
            "nullable": true
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "วันที่เอกสาร (back-date ได้)"
          },
          "shippedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "deliveredAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "เวลาระบบ (ใช้เรียง/แบ่งหน้า)"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderItem"
            }
          }
        }
      },
      "Product": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sku": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "barcode": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "type": "integer",
            "description": "ราคาปลีก (สตางค์)"
          },
          "priceWholesale": {
            "type": "integer",
            "nullable": true,
            "description": "ราคาส่ง (สตางค์)"
          },
          "priceVip": {
            "type": "integer",
            "nullable": true,
            "description": "ราคาสมาชิก (สตางค์)"
          },
          "stock": {
            "type": "integer",
            "description": "ยอดคงเหลือรวม"
          },
          "weightG": {
            "type": "integer",
            "description": "น้ำหนัก (กรัม)"
          },
          "dimensions": {
            "type": "string",
            "nullable": true,
            "description": "กว้างxยาวxสูง (ซม.)"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "nullable": true,
            "description": "path รูป — ต่อท้าย https://istatus.net"
          },
          "active": {
            "type": "boolean",
            "description": "false = ถูกปิดใช้งาน (เทียบเท่าลบ)"
          }
        }
      },
      "StockItem": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string",
            "format": "uuid"
          },
          "sku": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "stock": {
            "type": "integer",
            "description": "ยอดคงเหลือรวม (ค่าเดียวกับหน้าเว็บ)"
          }
        }
      },
      "Customer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "taxId": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "branch": {
            "type": "string",
            "nullable": true,
            "description": "สาขา (สำหรับใบกำกับภาษี)"
          },
          "contact": {
            "type": "string",
            "nullable": true
          },
          "points": {
            "type": "integer",
            "description": "แต้มสะสมคงเหลือ"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "shippingMethod": {
            "type": "string",
            "nullable": true
          },
          "priceTierId": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "ระดับราคาที่ผูกกับลูกค้า"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}
