ให้ ERP / โปรแกรมบัญชี / POS ของคุณดึงข้อมูล ออเดอร์รวมทุกช่องทาง · สินค้า · สต๊อก · ลูกค้า จาก istatus ได้โดยตรงด้วย API key เดียว — ไม่ต้องไปสมัครขอ API กับ Shopee / Lazada / TikTok เอง
Shopee ─┐ ┌─ ERP / โปรแกรมบัญชี / POS ของคุณ Lazada ─┼─ istatus (เชื่อมร้านของคุณอยู่แล้ว) ──┼─ GET /api/v1/... TikTok ─┤ รวมออเดอร์ / สินค้า / สต๊อก └─ Authorization: Bearer <API key> POS ────┘
GET /api/v1/orders — ออเดอร์รวมทุกช่อง พร้อมรายการสินค้าในบิล · shippingFee = ค่าจัดส่งที่ผู้ซื้อจ่าย (รวมในฐาน VAT/total)
shippingFeeStatus: confirmed = ค่าสุดท้ายแล้ว (รวมกรณีส่งฟรีจริง) · pending = ยังรอยอดจาก Shopee ให้ดึงซ้ำภายหลัง ค่า total/vat จะอัปเดตพร้อมกัน — ไม่ต้องเดาจาก shippingFee=0 อีกต่อไปGET /api/v1/orders/{id} — ใหม่ ออเดอร์รายใบ + รายการสินค้า (รูป/ชื่อ/จำนวน/ราคา)buyerPayment ใน /orders ทั้งสองเส้น — ใหม่ "ยอดที่ผู้ซื้อจ่ายตอนสั่ง" (หลังส่วนลดทุกชนิด รวมค่าส่งผู้ซื้อ · ก่อนหักการคืนเงินภายหลัง)
total (ฐานขาย/ภาษีของร้าน) และไม่ใช่ยอดเงินเข้าร้าน — เลือกใช้เลขให้ตรงงาน: กระทบยอดเงินเข้าใช้ /settlements · ที่มา: Shopee = escrow (ใบใหม่ status=pending ~30 นาทีแล้วเก็บ id ไว้ poll /orders/{id} ซ้ำ) · Lazada/TikTok = order detail · ใบเก่าที่ไม่มีหลักฐาน = unavailable (null ≠ 0 — และการ poll ใบ pending ไม่รับประกันจบที่ confirmed เสมอ เช่นใบ Shopee ที่จ่าย 0 ทั้งใบด้วยคูปอง)GET /api/v1/orders/{id}/fees — ใหม่ ค่าธรรมเนียม/ภาษีจริงต่อบิลแบบ breakdown (ค่าคอม · ค่าส่ง · VAT · หัก ณ ที่จ่าย · รอบกระทบยอด)
summary เป็นตัวเลขอ้างอิง (ก้อนเดียวกับที่ระบบลงบัญชี) — lines[] เป็นข้อมูลดิบรายบรรทัดจากแพลตฟอร์ม มีเครื่องหมายและแถวยอดรวมปนอยู่ ห้ามนำทุกบรรทัดมาบวกกันเอง · null ≠ 0 (แพลตฟอร์มไม่บอก ≠ ศูนย์)GET /api/v1/refunds — ใหม่ รายการคืนสินค้า/คืนเงิน + รายการสินค้าที่คืนGET /api/v1/stock/movements — ใหม่ ประวัติความเคลื่อนไหวสต๊อกทุกเหตุ (audit trail)GET /api/v1/settlements · /settlements/{id}/items — ใหม่ รอบกระทบยอดมาร์เก็ตเพลส + รายออเดอร์ในรอบ
basis: "recorded" = วันที่ระบบลงบัญชีจากข้อมูลแพลตฟอร์ม (ไม่ใช่วันเงินเข้าธนาคารจริง)PATCH /api/v1/orders/{id}/tracking — ใหม่ แก้เลขพัสดุ/ขนส่ง (เฉพาะใบ POS/ใบที่ส่งผ่าน API — ใบมาร์เก็ตเพลสแก้ไม่ได้)POST /api/v1/stock/pushes — ใหม่ สั่งดันสต็อกชุด SKU ขึ้นมาร์เก็ตเพลสทันที (ต้องเปิดสวิตช์ "ดันสต็อกขึ้น" ในแอปก่อน)GET /api/v1/products — สินค้า + ราคาขาย (ปลีก/ส่ง/สมาชิก)GET /api/v1/stock — ยอดสต๊อกคงเหลือ (ค่าเดียวกับหน้าเว็บ istatus)GET /api/v1/customers — ลูกค้าที่ร้านบันทึกไว้GET /api/v1/ping — เช็คว่า key ใช้ได้ + แพ็ก + วันทดลองคงเหลือPOST /api/v1/orders — ส่งออเดอร์จากระบบของคุณเข้า istatus (ตัดสต๊อก + ลงบัญชีให้เหมือนขายหน้าร้าน)POST /api/v1/products — สร้างสินค้าจากระบบของคุณ (ไม่ต้องกรอกในหน้าเว็บก่อนยิงออเดอร์)PUT /api/v1/stock/{sku} — ตั้งยอดสต๊อกคงเหลือ (ระบบออกใบปรับสต๊อกให้อัตโนมัติ)GET /api/v1/orders/held — ดูออเดอร์ที่ค้างห้องรอ เพราะโควตาออเดอร์ของแพ็กเต็ม
ฝั่งเขียนเปิดให้เป็นรายร้าน — ถ้าร้านคุณยังไม่ได้เปิด เส้นเขียนจะตอบ 404 (ทักเราเพื่อขอเปิด)
ist_ และแสดงเต็มครั้งเดียว — คัดลอกเก็บทันที (ระบบเก็บเป็นรหัสย่อยทางเดียว ดูซ้ำไม่ได้ · ออกได้สูงสุด 3 ใบต่อร้าน)export ISTATUS_API_KEY="ist_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"curl -s https://istatus.net/api/v1/ping \
-H "Authorization: Bearer $ISTATUS_API_KEY"
ถ้า key ใช้ได้จะตอบ:
{ "ok": true, "plan": "pro", "trialDaysLeft": null, "shopName": "ร้านของคุณ" }curl -s "https://istatus.net/api/v1/orders?from=2026-08-01&limit=20" \
-H "Authorization: Bearer $ISTATUS_API_KEY"ทุกคำขอต้องแนบ header:
Authorization: Bearer <API key ของร้าน>
401 { "code": "INVALID_KEY" }ISTATUS_API_KEY ให้เป็นแบบอย่างที่ถูกรวมยอดขายต่อช่วงเวลาให้เสร็จในคำขอเดียว — เหมาะกับ dashboard/รายงานฝั่งระบบของคุณ (นับวันเวลาไทย ตรงกับเมนูรายงานในแอป · หน่วยเงิน = สตางค์)
GET /api/v1/reports/sales?from=2026-09-01&to=2026-09-30&group=day
GET /api/v1/reports/sales?from=2026-01-01&to=2026-09-30&group=month&channel=shopee
| พารามิเตอร์ | ความหมาย |
|---|---|
from / to | บังคับ (YYYY-MM-DD, รวม to ด้วย) · ช่วงกว้างสุด 400 วัน |
group | day (ค่าเริ่มต้น) หรือ month |
channel | กรองช่องทาง: shopee | lazada | tiktok | woocommerce | pos | api (api = ออเดอร์ที่ส่งเข้าผ่าน POST /orders) |
{ "group": "day", "from": "2026-09-01", "to": "2026-09-02", "currencyUnit": "satang", "timezone": "Asia/Bangkok",
"rows": [ { "period": "2026-09-01", "orders": 12, "sales": 1250000, "refunded": 0,
"netSales": 1250000, "settledSales": 890000,
"byChannel": { "shopee": { "orders": 8, "sales": 890000, "refunded": 0 } } } ] }
sales/orders = บิลที่ไม่ถูกยกเลิก (void) · refunded หักตามวันที่คืนเงิน · netSales = sales − refundedsettledSales = เฉพาะบิลที่เงินเข้าแล้ว (status settled) — ใกล้เคียง "เงินเข้าจริง" ที่สุดที่ API ให้ตอนนี้ (ยังไม่หักค่าธรรมเนียมมาร์เก็ตเพลส)/orders) — POS/ออเดอร์จาก API ไม่จำกัดตัวเลขชุดเดียวกับรายงาน "กำไรตามช่องทาง" ในแอป (พารามิเตอร์เหมือน reports/sales ทุกตัว)
GET /api/v1/reports/profit?from=2026-09-01&to=2026-09-30&group=day
reports/sales)fee = ค่าธรรมเนียมมาร์เก็ตเพลส: ค่าจริงจากการกระทบยอด (settledFee) + ค่าประมาณของบิลที่ยังไม่กระทบยอด (estimatedFee — % จริงเฉลี่ยของร้าน หรืออัตราอ้างอิง) · มีประมาณเมื่อไหร่จะบอกใน warningsnetProfit = รายได้ก่อน VAT − ต้นทุนสินค้า (cogs) − ค่าธรรมเนียมหลังหักภาษีซื้อที่เคลมได้ (feeExVat) · marginPct = netProfit/รายได้ก่อน VATwarnings)ใหม่ 2026-09-03: เพิ่ม POST /api/v1/products — สร้างสินค้าจากระบบของคุณได้เลย ไม่ต้องกรอกในหน้าเว็บก่อนยิงออเดอร์ (ดูรายละเอียด)
เปลี่ยนแปลง 2026-09-02: Open API เปิดให้ฟรีทุกแพ็กเกจ ไม่จำกัดเวลา · โค้ด PLAN_REQUIRED/TRIAL_EXPIRED และ header X-Trial-Days-Left เลิกใช้ (ระบบไม่ส่งอีก — โค้ดเก่าที่เช็คไว้ไม่พังเพราะไม่เจอเคสนี้อีกเลย) · trialDaysLeft ใน /v1/me ตอบ null เสมอ
429 + Retry-AfterPLAN_REQUIRED / TRIAL_EXPIRED เลิกใช้แล้วตั้งแต่ 2026-09-02 (ระบบไม่ตอบโค้ดนี้อีก)429 { "code": "RATE_LIMITED" }Retry-After: <วินาที> —
โค้ดของคุณต้องรอตามค่านี้แล้วยิงใหม่ (ตัวอย่างโค้ดทุกภาษาด้านล่างทำให้แล้ว)from เป็นเวลาที่ดึงรอบก่อนพอlimit ได้สูงสุด 100 ต่อหน้า (ไม่ส่ง = 50)nextCursor — ถ้าไม่ใช่ null ให้ส่งกลับเป็นพารามิเตอร์ ?cursor= เพื่อดึงหน้าถัดไป วนจนได้ null = หมดแล้วcursor เป็น token ทึบ (มีลายเซ็นกันแก้) — ใช้ค่าที่ระบบให้เท่านั้น ประกอบเอง/แก้เองจะได้ 422 INVALID_CURSOR25000 = 250.00 บาท · หาร 100 ก่อนแสดงผลเสมอvatRate เป็นหน่วยต่อ 10000 — 700 = 7.00%Z) — เวลาไทย = +7 ชั่วโมง · พารามิเตอร์ from/to เป็นวันที่ YYYY-MM-DD ตีความตามเขตเวลาระบบPOST/PUT) และ
GET /api/v1/orders/held จะตอบ 404 — ทักทีมงานเพื่อขอเปิดexternalOrderNo: ส่งเลขออเดอร์ในระบบคุณมาเสมอ —
ยิงซ้ำเลขเดิมจะได้ 200 พร้อมผลเดิม ไม่เกิดบิลซ้ำ (เน็ตหลุดกลางทางแล้วยิงใหม่ได้อย่างปลอดภัย) ·
เลขบิลใน istatus = API-<externalOrderNo> · เลขนี้ผูกตลอดชีพ: ถ้าเจ้าของร้านยกเลิก (void) บิลไปแล้ว ยิงเลขเดิมซ้ำจะได้ { "status": "voided" } ไม่สร้างใหม่ — อยากบันทึกใหม่จริงต้องใช้ externalOrderNo ใหม่202 ไม่ใช่ error: ถ้าโควตาออเดอร์ของแพ็กเดือนนี้เต็ม ระบบรับออเดอร์ไว้ในห้องรอ
({ "status": "held" }) แล้วบันทึกเข้าให้เองเมื่ออัปเกรดแพ็กหรือขึ้นเดือนใหม่ —
ห้ามยิงซ้ำ ห้ามถือว่าล้มเหลว · ดูคิวได้ที่ GET /api/v1/orders/held และเจ้าของร้านเห็นป้ายเตือนในแอป ·
ห้องรอรับได้ 5,000 ใบ/ร้าน — เกินแล้วใบใหม่ได้ 409 HELD_FULL (ของที่รับไว้แล้วไม่หาย)rejected = ยังไม่เป็นยอดขาย —
โค้ดของคุณควรเช็ค rejectedCount จาก GET /api/v1/ping หรือ rejectedRows จาก
GET /api/v1/orders/held เป็นประจำ แก้ตาม lastError แล้วส่ง externalOrderNo เดิมมาใหม่quarantined — ไม่บันทึก ไม่ลบ)
เห็นได้ใน quarantinedRows ของ GET /api/v1/orders/held · ส่ง externalOrderNo เดิมด้วย key ที่ใช้งานได้ ระบบจะโอนใบกลับเข้าคิวให้อัตโนมัติsku + qty — ระบบคิดราคาจากราคาขายในแคตตาล็อกของร้าน
(ตามระดับราคาของลูกค้า ถ้าส่ง customerId) · ไม่รับราคา/ส่วนลดจากภายนอก และส่ง field ที่ไม่รู้จักมาจะได้
422 VALIDATION ทันที (ไม่ตัดทิ้งเงียบ)409 INSUFFICIENT_STOCK พร้อมบอก SKU ที่ขาด (กติกาเดียวกับการขายหน้าร้าน) —
ทั้งบิลไม่ถูกบันทึกแม้แต่รายการเดียวPUT /api/v1/stock/{sku} เป็นการตั้งยอดคงเหลือ (absolute) ไม่ใช่บวก/ลบ — ระบบออกใบปรับสต๊อกและลงบัญชีให้
· ยอดตรงอยู่แล้วจะไม่สร้างใบเปล่า (delta: 0, adjusted: false)
· แนะนำส่ง expectedQty = ยอดที่คุณอ่านไปล่าสุด — ถ้าหน้าร้านขายไประหว่างนั้นจนยอดจริงไม่ตรง จะได้ 409 STALE_STOCK แทนการทับยอดด้วยข้อมูลเก่า (ไม่งั้นของที่ขายไปแล้วจะถูกคืนเข้าสต๊อกเงียบ ๆ)channel เป็น shopee / lazada / tiktok / woocommerce
ระบบจำกัดข้อมูลย้อนหลัง 90 วัน ตามนโยบายคุ้มครองข้อมูลของแพลตฟอร์ม —
response จะบอก "mpLimited": true, "retentionDays": 90{ "code": "RATE_LIMITED", "message": "ยิงถี่เกินกำหนด — ดู header Retry-After แล้วลองใหม่" }
ให้โค้ดของคุณตัดสินใจจาก code (คงที่ เครื่องอ่านได้) — message เป็นภาษาไทยสำหรับคน อาจเปลี่ยนถ้อยคำได้
| HTTP | code | ความหมาย | โค้ดของคุณควรทำ |
|---|---|---|---|
| 401 | INVALID_KEY | key ผิด / ถูกเพิกถอน / ไม่ได้ส่ง | หยุด — ตรวจ key ใน env / ออกใบใหม่ |
| 422 | INVALID_PARAM | พารามิเตอร์ผิดรูปแบบ (เช่น from ไม่ใช่ YYYY-MM-DD) | แก้พารามิเตอร์ — อย่ายิงซ้ำแบบเดิม |
| 422 | INVALID_CURSOR | cursor ไม่ใช่ค่าที่ระบบให้ | เริ่มดึงใหม่จากหน้าแรก (ไม่ส่ง cursor) |
| 429 | RATE_LIMITED | ยิงถี่เกินฟิวส์ต่อร้าน | รอตาม header Retry-After แล้วยิงใหม่ |
| 404 | — | ร้านนี้ยังไม่ได้เปิดฝั่งเขียน (เส้น POST/PUT/orders/held) | หยุด — ทักทีมงานเพื่อขอเปิด |
| 404 | NOT_FOUND | ไม่พบ SKU ที่ระบุใน PUT /stock/{sku} | ตรวจ SKU กับ GET /api/v1/products |
| 409 | INSUFFICIENT_STOCK | สต๊อกไม่พอ — ทั้งบิลไม่ถูกบันทึก | เติมสต๊อกแล้วยิงใหม่ (เลขเดิมได้ ไม่ซ้ำ) |
| 409 | HELD_FULL | ห้องรอของร้านเต็ม (5,000 ใบ) — ใบนี้ยังไม่ถูกรับ | หยุด — แจ้งเจ้าของร้านอัปเกรดแพ็กเพื่อปล่อยคิว แล้วส่งเลขเดิมใหม่ |
| 409 | STALE_STOCK | PUT /stock: ยอดจริงไม่ตรง expectedQty (มีการขาย/ปรับระหว่างนั้น) | อ่านยอดใหม่จาก GET /stock แล้วส่งอีกครั้ง — ไม่มีอะไรถูกแตะ |
| 409 | IN_PROGRESS | POST /orders: เลขนี้กำลังถูกบันทึกโดยคำขออื่นพร้อมกัน (เช่น ยิงซ้ำเร็วเกินไป) | รอ 2 วินาทีแล้วส่งเลขเดิมซ้ำ — จะได้ผลจริง (created / held / voided) |
| 503 | RETRY | เลขใบปรับสต๊อกชนกัน (พบยากมาก) — ยอดยังไม่ถูกแตะ | ยิง PUT /stock ใบเดิมซ้ำได้ทันที |
| 422 | VALIDATION | body ผิดรูปแบบ / มี field ที่ไม่รู้จัก | แก้ body ตามข้อความ — อย่ายิงซ้ำแบบเดิม |
| 422 | UNKNOWN_SKU | ไม่มี SKU นี้ในร้าน (หรือถูกปิดใช้งาน) | สร้าง/เปิดสินค้าในแอปก่อน แล้วยิงใหม่ |
| 422 | BUNDLE_SKU | ตั้งสต๊อกให้สินค้าจัดชุดโดยตรงไม่ได้ (ยอดคำนวณจากสินค้าฐาน) | ตั้งยอดที่สินค้าฐานแทน |
| 500 | INTERNAL | ข้อผิดพลาดฝั่งระบบ | รอครู่แล้วลองใหม่ — ถ้าค้างนานติดต่อเรา |
201 = บันทึกแล้ว · 202 = เข้าห้องรอ (สำเร็จ ไม่ต้องยิงซ้ำ) ·
200 = เลขนี้เคยส่งแล้ว ได้ผลเดิม — ทั้งสามอย่างนี้ถือว่า "ส่งสำเร็จ" ในโค้ดของคุณ
ทุกภาษาเป็นโครงเดียวกัน: อ่าน key จาก env → ยิง GET → โดน 429 = รอตาม Retry-After แล้วยิงใหม่อัตโนมัติ →
error อื่นโยนพร้อม code → มีฟังก์ชัน ไล่ cursor จนหมดทุกหน้า ให้พร้อม
· ตัวอย่างรายเมนูใน อ้างอิง API เรียกใช้ฟังก์ชันจากโครงนี้
# istatus-pull.sh — ต้องมี curl + jq
# เก็บ key ใน env เท่านั้น: export ISTATUS_API_KEY="ist_..." (ห้ามพิมพ์ key ลงไฟล์สคริปต์/git)
BASE="https://istatus.net"
# ดึงรายการแบบไล่ cursor จนหมด + รอเองเมื่อโดนฟิวส์ 429 (อ่าน Retry-After)
pull_all() { # ใช้: pull_all /api/v1/orders "from=2026-08-01"
local path="$1" query="$2" cursor="" res http wait
while :; do
res=$(curl -s -w '\n%{http_code}' -D /tmp/istatus-h -G "$BASE$path" \
-H "Authorization: Bearer $ISTATUS_API_KEY" \
--data-urlencode "limit=100" \
${query:+--data-urlencode "$query"} \
${cursor:+--data-urlencode "cursor=$cursor"})
http=${res##*$'\n'}; res=${res%$'\n'*}
if [ "$http" = "429" ]; then # ฟิวส์ความถี่ — รอตาม Retry-After แล้วยิงใหม่
wait=$(grep -i '^retry-after' /tmp/istatus-h | tr -dc '0-9'); sleep "${wait:-1}"; continue
fi
if [ "$http" != "200" ]; then echo "$res" | jq -r '"ERROR " + .code + ": " + .message' >&2; return 1; fi
echo "$res" | jq -c '.rows[]' # หนึ่งบรรทัดต่อหนึ่งรายการ — เอาไป pipe ต่อได้
cursor=$(echo "$res" | jq -r '.nextCursor // empty')
[ -z "$cursor" ] && break # nextCursor = null → หมดแล้ว
done
}
<?php
// istatus-client.php — PHP 8.0+ (ext-curl)
// เก็บ key ใน env เท่านั้น (.env / ตั้งใน server config) — ห้าม hardcode ในโค้ด
const ISTATUS_BASE = 'https://istatus.net';
function istatus_get(string $path, array $params = []): array {
$key = getenv('ISTATUS_API_KEY') ?: throw new RuntimeException('ตั้ง env ISTATUS_API_KEY ก่อน');
$params = array_filter($params, fn($v) => $v !== null && $v !== '');
$url = ISTATUS_BASE . $path . ($params ? '?' . http_build_query($params) : '');
for ($attempt = 0; $attempt < 5; $attempt++) {
$retryAfter = 1;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
// อ่านค่า Retry-After จาก header ไว้ใช้ตอนโดนฟิวส์ 429
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) {
if (stripos($line, 'Retry-After:') === 0) $retryAfter = max(1, (int) trim(substr($line, 12)));
return strlen($line);
},
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 429) { sleep($retryAfter); continue; } // ฟิวส์ความถี่ — รอแล้วยิงใหม่
$body = json_decode((string) $raw, true) ?? [];
if ($status >= 400) { // ตัดสินใจจาก code เครื่องอ่านได้
throw new RuntimeException(($body['code'] ?? "HTTP $status") . ': ' . ($body['message'] ?? 'error'));
}
return $body;
}
throw new RuntimeException('RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก');
}
// ไล่ทุกหน้าด้วย cursor จนหมด (ใช้กับทุก endpoint ที่เป็นรายการ)
function istatus_get_all(string $path, array $params = []): array {
$rows = []; $cursor = null;
do {
$page = istatus_get($path, array_merge($params, ['limit' => 100, 'cursor' => $cursor]));
$rows = array_merge($rows, $page['rows']);
$cursor = $page['nextCursor'] ?? null;
} while ($cursor);
return $rows;
}
// istatus-client.mjs — Node.js 18+ (ใช้ fetch ในตัว ไม่ต้องลงแพ็กเกจ)
// เก็บ key ใน env เท่านั้น: export ISTATUS_API_KEY=ist_... (ห้าม hardcode ในโค้ด/คอมมิตลง git)
const BASE = 'https://istatus.net';
const KEY = process.env.ISTATUS_API_KEY;
if (!KEY) throw new Error('ตั้ง env ISTATUS_API_KEY ก่อน');
export async function istatusGet(path, params = {}) {
const url = new URL(path, BASE);
for (const [k, v] of Object.entries(params)) if (v != null && v !== '') url.searchParams.set(k, v);
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (res.status === 429) { // ฟิวส์ความถี่ — รอตาม Retry-After แล้วยิงใหม่
const wait = Number(res.headers.get('retry-after')) || 1;
await new Promise(r => setTimeout(r, wait * 1000));
continue;
}
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.message}`); // ตัดสินใจจาก code เครื่องอ่านได้
return body;
}
throw new Error('RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก');
}
// ไล่ทุกหน้าด้วย cursor จนหมด (ใช้กับทุก endpoint ที่เป็นรายการ)
export async function istatusGetAll(path, params = {}) {
const rows = [];
let cursor = null;
do {
const page = await istatusGet(path, { ...params, limit: 100, cursor });
rows.push(...page.rows);
cursor = page.nextCursor;
} while (cursor);
return rows;
}
# istatus_client.py — Python 3.9+ (pip install requests)
# เก็บ key ใน env เท่านั้น: export ISTATUS_API_KEY=ist_... (ห้าม hardcode ในโค้ด/คอมมิตลง git)
import os, time, requests
BASE = "https://istatus.net"
KEY = os.environ["ISTATUS_API_KEY"] # ไม่มี env = ให้พังตั้งแต่เริ่ม ดีกว่ายิงด้วย key ว่าง
def istatus_get(path: str, params: dict | None = None) -> dict:
for _attempt in range(5):
res = requests.get(
BASE + path,
params={k: v for k, v in (params or {}).items() if v is not None},
headers={"Authorization": f"Bearer {KEY}"},
timeout=30,
)
if res.status_code == 429: # ฟิวส์ความถี่ — รอตาม Retry-After แล้วยิงใหม่
time.sleep(int(res.headers.get("Retry-After", "1")))
continue
body = res.json()
if not res.ok: # ตัดสินใจจาก code เครื่องอ่านได้
raise RuntimeError(f"{body.get('code')}: {body.get('message')}")
return body
raise RuntimeError("RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก")
def istatus_get_all(path: str, params: dict | None = None) -> list:
"""ไล่ทุกหน้าด้วย cursor จนหมด (ใช้กับทุก endpoint ที่เป็นรายการ)"""
rows, cursor = [], None
while True:
page = istatus_get(path, {**(params or {}), "limit": 100, "cursor": cursor})
rows += page["rows"]
cursor = page.get("nextCursor")
if not cursor:
return rows
// IstatusClient.cs — .NET 6+
// เก็บ key ใน env เท่านั้น (ห้าม hardcode ในโค้ด/คอมมิตลง git)
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
public static class IstatusClient
{
static readonly HttpClient Http = new() { BaseAddress = new Uri("https://istatus.net") };
static readonly string Key = Environment.GetEnvironmentVariable("ISTATUS_API_KEY")
?? throw new InvalidOperationException("ตั้ง env ISTATUS_API_KEY ก่อน");
public static async Task<JsonElement> GetAsync(string path, Dictionary<string, string?>? query = null)
{
var qs = query == null ? "" : string.Join("&", query
.Where(kv => !string.IsNullOrEmpty(kv.Value))
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}"));
var url = path + (qs.Length > 0 ? "?" + qs : "");
for (var attempt = 0; attempt < 5; attempt++)
{
using var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Key);
using var res = await Http.SendAsync(req);
if (res.StatusCode == HttpStatusCode.TooManyRequests)
{ // ฟิวส์ความถี่ — รอตาม Retry-After แล้วยิงใหม่
await Task.Delay(res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1));
continue;
}
var body = JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
if (!res.IsSuccessStatusCode) // ตัดสินใจจาก code เครื่องอ่านได้
throw new Exception($"{body.GetProperty("code").GetString()}: {body.GetProperty("message").GetString()}");
return body;
}
throw new Exception("RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก");
}
// ไล่ทุกหน้าด้วย cursor จนหมด (ใช้กับทุก endpoint ที่เป็นรายการ)
public static async Task<List<JsonElement>> GetAllAsync(string path, Dictionary<string, string?>? query = null)
{
var rows = new List<JsonElement>();
string? cursor = null;
do
{
var q = new Dictionary<string, string?>(query ?? new()) { ["limit"] = "100", ["cursor"] = cursor };
var page = await GetAsync(path, q);
rows.AddRange(page.GetProperty("rows").EnumerateArray().Select(e => e.Clone()));
var next = page.GetProperty("nextCursor");
cursor = next.ValueKind == JsonValueKind.String ? next.GetString() : null;
} while (cursor != null);
return rows;
}
}
POST / PUT
ต่อจากโครงหลักด้านบน — ต่างกันตรงที่ฝั่งเขียนต้องอ่านรหัสสถานะด้วย:
201 บันทึกแล้ว · 202 เข้าห้องรอ (โควตาเต็ม — สำเร็จ ห้ามยิงซ้ำ) ·
200 เลขนี้เคยส่งแล้ว (กันบิลซ้ำให้อัตโนมัติ)
# ── ส่งข้อมูลเข้า (POST/PUT) — ต่อจากโครงหลักด้านบน ──
# สำคัญ: ต้องดู "รหัสสถานะ" ด้วย ไม่ใช่ดูแต่ body — 201 บันทึกแล้ว · 202 เข้าห้องรอ (สำเร็จ!) · 200 เลขนี้เคยส่งแล้ว
istatus_send() { # ใช้: istatus_send POST /api/v1/orders '{"externalOrderNo":"PO-1","items":[{"sku":"A-001","qty":2}]}'
local method="$1" path="$2" data="$3" res http wait
while :; do
res=$(curl -s -w '\n%{http_code}' -D /tmp/istatus-h -X "$method" "$BASE$path" \
-H "Authorization: Bearer $ISTATUS_API_KEY" -H 'Content-Type: application/json' -d "$data")
http=${res##*$'\n'}; res=${res%$'\n'*}
if [ "$http" = "429" ]; then # ฟิวส์ความถี่ (เขียน ~2/วิ) — รอตาม Retry-After แล้วยิงใหม่
wait=$(grep -i '^retry-after' /tmp/istatus-h | tr -dc '0-9'); sleep "${wait:-1}"; continue
fi
break
done
case "$http" in
201) echo "created $res";; # บันทึกเป็นบิลจริงแล้ว
202) echo "held $res";; # โควตาแพ็กเต็ม → เข้าห้องรอ ระบบจะบันทึกให้เอง — ห้ามยิงซ้ำ
200) echo "already $res";; # เลขนี้เคยส่งแล้ว (idempotent) = ไม่มีบิลซ้ำ
*) echo "ERROR $http $(echo "$res" | jq -r '.code + ": " + .message')" >&2; return 1;;
esac
}
// ── ส่งข้อมูลเข้า (POST/PUT) — ต่อจากโครงหลักด้านบน ──
// คืน status มาด้วยเสมอ เพราะ 201 (บันทึกแล้ว) / 202 (เข้าห้องรอ) / 200 (เคยส่งแล้ว) คนละความหมาย แต่ "สำเร็จ" ทั้งหมด
function istatus_send(string $method, string $path, array $payload): array {
$key = getenv('ISTATUS_API_KEY') ?: throw new RuntimeException('ตั้ง env ISTATUS_API_KEY ก่อน');
for ($attempt = 0; $attempt < 5; $attempt++) {
$retryAfter = 1;
$ch = curl_init(ISTATUS_BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) {
if (stripos($line, 'Retry-After:') === 0) $retryAfter = max(1, (int) trim(substr($line, 12)));
return strlen($line);
},
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 429) { sleep($retryAfter); continue; }
$body = json_decode((string) $raw, true) ?? [];
if ($status >= 400) {
throw new RuntimeException(($body['code'] ?? "HTTP $status") . ': ' . ($body['message'] ?? 'error'));
}
return ['status' => $status, 'body' => $body];
}
throw new RuntimeException('RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก');
}
// ── ส่งข้อมูลเข้า (POST/PUT) — ต่อจากโครงหลักด้านบน ──
// คืน { status, body } เสมอ: 201 บันทึกแล้ว · 202 เข้าห้องรอ (สำเร็จ ห้ามยิงซ้ำ) · 200 เลขนี้เคยส่งแล้ว
export async function istatusSend(method, path, payload) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(new URL(path, BASE), {
method,
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.status === 429) { // ฟิวส์ความถี่ (เขียน ~2/วิ)
const wait = Number(res.headers.get('retry-after')) || 1;
await new Promise(r => setTimeout(r, wait * 1000));
continue;
}
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.message}`);
return { status: res.status, body };
}
throw new Error('RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก');
}
# ── ส่งข้อมูลเข้า (POST/PUT) — ต่อจากโครงหลักด้านบน ──
# คืน (status, body): 201 บันทึกแล้ว · 202 เข้าห้องรอ (สำเร็จ ห้ามยิงซ้ำ) · 200 เลขนี้เคยส่งแล้ว
def istatus_send(method: str, path: str, payload: dict) -> tuple:
for _attempt in range(5):
res = requests.request(
method, BASE + path, json=payload,
headers={"Authorization": f"Bearer {KEY}"}, timeout=30,
)
if res.status_code == 429: # ฟิวส์ความถี่ (เขียน ~2/วิ)
time.sleep(int(res.headers.get("Retry-After", "1")))
continue
body = res.json()
if not res.ok:
raise RuntimeError(f"{body.get('code')}: {body.get('message')}")
return res.status_code, body
raise RuntimeError("RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก")
// ── ส่งข้อมูลเข้า (POST/PUT) — เพิ่มเมธอดนี้ในคลาส IstatusClient เดิม ──
// คืน (Status, Body): 201 บันทึกแล้ว · 202 เข้าห้องรอ (สำเร็จ ห้ามยิงซ้ำ) · 200 เลขนี้เคยส่งแล้ว
public static async Task<(int Status, JsonElement Body)> SendAsync(HttpMethod method, string path, object payload)
{
for (var attempt = 0; attempt < 5; attempt++)
{
using var req = new HttpRequestMessage(method, path)
{
Content = new StringContent(JsonSerializer.Serialize(payload),
System.Text.Encoding.UTF8, "application/json"),
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Key);
using var res = await Http.SendAsync(req);
if (res.StatusCode == HttpStatusCode.TooManyRequests)
{ // ฟิวส์ความถี่ (เขียน ~2/วิ) — รอตาม Retry-After แล้วยิงใหม่
await Task.Delay(res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1));
continue;
}
var body = JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
if (!res.IsSuccessStatusCode)
throw new Exception($"{body.GetProperty("code").GetString()}: {body.GetProperty("message").GetString()}");
return ((int)res.StatusCode, body);
}
throw new Exception("RATE_LIMITED: ยิงซ้ำ 5 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก");
}
Base URL: https://istatus.net · ทุกเส้นต้องแนบ Authorization: Bearer ·
เส้นอ่านเป็น GET · เส้นเขียน (POST /orders, PUT /stock/{sku}) เปิดให้เป็นรายร้าน — ปิดอยู่ = 404 ·
ทุก response เป็น JSON (UTF-8) · error ที่เป็นไปได้ทุกเส้น: 401 / 403 / 422 / 429 (ดูตาราง error)
เช็คว่า key ใช้ได้ + แพ็กปัจจุบัน — ใช้เป็นคำขอแรกตอนตั้งค่าระบบ และใช้เช็คสุขภาพการเชื่อมต่อ
พารามิเตอร์: ไม่มี
{
"ok": true,
"plan": "starter",
"trialDaysLeft": null,
"shopName": "ร้านตัวอย่าง"
}
| field | ชนิด | ความหมาย |
|---|---|---|
ok | boolean | true เสมอเมื่อมาถึงขั้นนี้ (key + สิทธิ์ผ่านแล้ว) |
plan | string | แพ็กปัจจุบัน: free | starter | pro | business | scale |
trialDaysLeft | null | null เสมอ — Open API ฟรีทุกแพ็กแล้ว (field คงไว้เพื่อความเข้ากันได้ของโค้ดเก่า) |
shopName | string | null | ชื่อร้าน (ไว้ยืนยันว่า key ตรงร้าน) |
curl -s https://istatus.net/api/v1/ping \
-H "Authorization: Bearer $ISTATUS_API_KEY"
// ใช้ istatus_get() จากโครงหลักด้านบน
print_r(istatus_get('/api/v1/ping'));
// ใช้ istatusGet() จากโครงหลักด้านบน
console.log(await istatusGet('/api/v1/ping'));
# ใช้ istatus_get() จากโครงหลักด้านบน
print(istatus_get("/api/v1/ping"))
// ใช้ IstatusClient จากโครงหลักด้านบน
var ping = await IstatusClient.GetAsync("/api/v1/ping");
Console.WriteLine(ping.GetProperty("shopName").GetString());
รายการออเดอร์รวมทุกช่องทาง (POS / Shopee / Lazada / TikTok / WooCommerce ฯลฯ) เรียงจากใหม่ไปเก่า —
แต่ละออเดอร์แนบ items[] (รายการสินค้าในบิล) มาให้ครบในคำขอเดียว
| พารามิเตอร์ | ชนิด | ความหมาย |
|---|---|---|
from | date | วันที่เริ่ม YYYY-MM-DD (นับตั้งแต่ต้นวัน) |
to | date | วันที่สิ้นสุด YYYY-MM-DD (รวมทั้งวัน) |
channel | string | กรองช่องทาง: pos, shopee, lazada, tiktok, woocommerce, web |
channel=api | string | เฉพาะออเดอร์ที่ระบบของคุณส่งเข้ามาเองผ่าน POST /orders — ทุกแถวมี field origin (api / pos / shopee …) ให้แยก "ของที่ ERP ส่ง" ออกจากขายหน้าร้านจริง กัน sync กลับซ้ำ |
status | string | กรองสถานะ เช่น paid (ขายแล้ว), voided (ยกเลิกบิล) |
cursor | string | ค่า nextCursor จากหน้าก่อน |
limit | integer | จำนวนต่อหน้า 1-100 (ไม่ส่ง = 50) |
channel เป็นช่อง marketplace = ข้อมูลย้อนหลังไม่เกิน 90 วัน (response มี mpLimited: true) — ไม่กรอง channel หรือกรอง pos ไม่ติดข้อจำกัดนี้{
"rows": [
{
"id": "0d3f2a10-1111-4222-8333-944455556666",
"orderNo": "POS-1756600000000-123",
"channel": "pos",
"origin": "pos",
"status": "paid",
"priceType": "retail",
"total": 25000, // สตางค์ = 250.00 บาท (ยอดที่ลูกค้าจ่ายจริง รวม VAT)
"vat": 1636, // VAT ในบิล (สตางค์)
"vatRate": 700, // 7.00% (หน่วยต่อ 10000)
"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, // ไม่ null = ส่งกลับเป็น ?cursor= เพื่อดึงหน้าถัดไป
"mpLimited": false,
"retentionDays": null
}
# หน้าเดียว
curl -s -G https://istatus.net/api/v1/orders \
-H "Authorization: Bearer $ISTATUS_API_KEY" \
--data-urlencode "from=2026-08-01" --data-urlencode "to=2026-08-31" \
--data-urlencode "status=paid" --data-urlencode "limit=100"
# ทุกหน้า (ไล่ cursor + รอเมื่อโดน 429) — ใช้ pull_all จากโครงหลักด้านบน
pull_all /api/v1/orders "from=2026-08-01" | jq -r '[.orderNo, .channel, (.total/100)] | @tsv'
// ออเดอร์ที่ขายแล้วของเดือนสิงหา — ทุกหน้า (istatus_get_all ไล่ cursor + รอ 429 ให้เอง)
$orders = istatus_get_all('/api/v1/orders', [
'from' => '2026-08-01', 'to' => '2026-08-31', 'status' => 'paid',
]);
foreach ($orders as $o) {
printf("%s %s %.2f บาท\n", $o['orderNo'], $o['channel'], $o['total'] / 100);
foreach ($o['items'] as $it) {
printf(" - %s x%d @ %.2f\n", $it['sku'], $it['qty'], $it['price'] / 100);
}
}
// ออเดอร์ที่ขายแล้วของเดือนสิงหา — ทุกหน้า (istatusGetAll ไล่ cursor + รอ 429 ให้เอง)
const orders = await istatusGetAll('/api/v1/orders', {
from: '2026-08-01', to: '2026-08-31', status: 'paid',
});
for (const o of orders) {
console.log(o.orderNo, o.channel, (o.total / 100).toFixed(2), 'บาท');
for (const it of o.items) console.log(' -', it.sku, 'x' + it.qty, '@', (it.price / 100).toFixed(2));
}
# ออเดอร์ที่ขายแล้วของเดือนสิงหา — ทุกหน้า (istatus_get_all ไล่ cursor + รอ 429 ให้เอง)
orders = istatus_get_all("/api/v1/orders", {
"from": "2026-08-01", "to": "2026-08-31", "status": "paid",
})
for o in orders:
print(o["orderNo"], o["channel"], o["total"] / 100, "บาท")
for it in o["items"]:
print(" -", it["sku"], "x", it["qty"], "@", it["price"] / 100)
// ออเดอร์ที่ขายแล้วของเดือนสิงหา — ทุกหน้า (GetAllAsync ไล่ cursor + รอ 429 ให้เอง)
var orders = await IstatusClient.GetAllAsync("/api/v1/orders", new()
{
["from"] = "2026-08-01", ["to"] = "2026-08-31", ["status"] = "paid",
});
foreach (var o in orders)
{
Console.WriteLine($"{o.GetProperty("orderNo").GetString()} {o.GetProperty("channel").GetString()} {o.GetProperty("total").GetInt64() / 100.0:F2} บาท");
}
สร้างสินค้าใหม่จากระบบของคุณ (ระบบคุณเป็น master ไม่ต้องกรอกในหน้าเว็บ) — สร้างเสร็จใช้ SKU นั้นยิงออเดอร์
POST /api/v1/orders ได้ทันที · เดินเส้นทางเดียวกับการเพิ่มสินค้าหน้าเว็บทุกอย่าง:
เพดานจำนวนสินค้าตามแพ็กเกจ · กู้คืน SKU ที่เคยถูกลบ · กันชนกับ SKU ที่ยังผูก listing ของสินค้าอื่น
| field | ชนิด | ความหมาย |
|---|---|---|
sku | string · จำเป็น | รหัสสินค้า (ระบบตัดช่องว่าง/ขึ้นบรรทัดหัวท้ายให้) |
name | string · จำเป็น | ชื่อสินค้า |
price | integer | ราคาขายปลีก หน่วยสตางค์ (ไม่ส่ง = 0 · ไม่เกิน 1,000,000,000) |
cost | integer | ต้นทุนต่อหน่วย หน่วยสตางค์ — ใส่ไว้ระบบคำนวณกำไรจริงต่อบิลให้ |
barcode category weightG dimensions description | optional | บาร์โค้ด · หมวดหมู่ · น้ำหนัก (กรัม) · ขนาด · คำอธิบาย |
สต๊อก: สินค้าใหม่เริ่มที่ 0 เสมอ — ตั้งยอดตั้งต้นด้วย PUT /api/v1/stock/{sku} อีกหนึ่งคำสั่ง
(ระบบออกใบปรับสต๊อก + ลงบัญชีให้ถูกต้อง ตรวจสอบย้อนหลังได้)
curl -s -X POST https://istatus.net/api/v1/products \
-H "Authorization: Bearer $ISTATUS_API_KEY" -H "Content-Type: application/json" \
-d '{ "sku": "ERP-001", "name": "ปุ๋ยเคมี 15-15-15 กระสอบ 50 กก.",
"price": 85000, "cost": 62000 }'
# 201 → { "status": "created", "id": "8f2c...", "sku": "ERP-001", "price": 85000, "cost": 62000, "stock": 0 }
# ตั้งสต๊อกตั้งต้น
curl -s -X PUT https://istatus.net/api/v1/stock/ERP-001 \
-H "Authorization: Bearer $ISTATUS_API_KEY" -H "Content-Type: application/json" \
-d '{ "qty": 100, "note": "ยอดตั้งต้นจาก ERP" }'
status: "recovered" = SKU นี้เคยถูกลบแล้วถูกกู้คืน (ได้ id เดิม ไม่ใช่แถวใหม่) — ถ้าแถวเดิมยังมียอด/ต้นทุนค้างอยู่ ระบบจะคง stock/cost เดิมไว้แทนค่าที่ส่งมา · response คืนค่าที่อยู่ในฐานจริงเสมอ — เทียบ cost/stock กับที่ส่งไป ถ้าตั้งใจเปลี่ยนต้นทุนให้แก้ที่หน้าแก้ไขสินค้า (สต๊อกแก้ผ่าน PUT /stock/{sku})
error ที่เจอได้: 409 DUPLICATE_SKU (SKU นี้มีสินค้าอยู่แล้ว) · 409 SKU_LINKED (ชื่อนี้ยังผูกกับสินค้าอื่นผ่าน listing — ปลดการผูกก่อน)
· 409 DUPLICATE_BARCODE / 409 CONFLICT (บาร์โค้ด/SKU ชนกัน รวมถึงชนข้ามช่อง sku↔barcode)
· 403 PLAN_LIMIT (จำนวนสินค้าชนเพดานแพ็ก) · 422 VALIDATION
รายการสินค้า + ราคาขาย เรียงตาม SKU (A→Z) — คืนทั้งสินค้าที่เปิดและปิดใช้งานพร้อม flag active
(ระบบที่ sync ต้องเห็นตัวที่ถูกปิดด้วย จะได้ไม่ค้างในระบบคุณ) · ไม่มีข้อมูลต้นทุน
| พารามิเตอร์ | ชนิด | ความหมาย |
|---|---|---|
q | string | ค้นหาจาก SKU / ชื่อสินค้า (บางส่วน) หรือบาร์โค้ด (ตรงตัว) |
cursor | string | ค่า nextCursor จากหน้าก่อน |
limit | integer | จำนวนต่อหน้า 1-100 (ไม่ส่ง = 50) |
{
"rows": [
{
"id": "7b1c0000-2222-4333-8444-955566667777",
"sku": "A-001",
"name": "สินค้าตัวอย่าง",
"category": "ทั่วไป",
"barcode": "8850000000001",
"price": 12500, // ราคาปลีก (สตางค์) = 125.00 บาท
"priceWholesale": 11000, // ราคาส่ง (null = ไม่ตั้ง)
"priceVip": null, // ราคาสมาชิก
"stock": 42, // ยอดคงเหลือรวม
"weightG": 500, // น้ำหนัก (กรัม)
"dimensions": "20x15x10", // กว้างxยาวxสูง (ซม.)
"description": null,
"imageUrl": "/uploads/img-xxxx.jpg", // ต่อท้าย https://istatus.net เอง
"active": true // false = ถูกปิดใช้งาน (เทียบเท่าลบ)
}
],
"nextCursor": null
}
# ค้นหาสินค้า
curl -s -G https://istatus.net/api/v1/products \
-H "Authorization: Bearer $ISTATUS_API_KEY" \
--data-urlencode "q=A-0" --data-urlencode "limit=100"
# sync สินค้าทั้งร้าน (ทุกหน้า)
pull_all /api/v1/products "" | jq -r '[.sku, .name, (.price/100), .stock, .active] | @tsv'
// sync สินค้าทั้งร้านเข้าฐานคุณ — ทุกหน้า
$products = istatus_get_all('/api/v1/products');
foreach ($products as $p) {
if (!$p['active']) { /* ปิดใช้งานฝั่งคุณด้วย */ continue; }
printf("%s %s %.2f บาท คงเหลือ %d\n", $p['sku'], $p['name'], $p['price'] / 100, $p['stock']);
}
// sync สินค้าทั้งร้านเข้าฐานคุณ — ทุกหน้า
const products = await istatusGetAll('/api/v1/products');
for (const p of products) {
if (!p.active) continue; // ตัวที่ถูกปิด — ปิดใช้งานฝั่งคุณด้วย
console.log(p.sku, p.name, (p.price / 100).toFixed(2), 'บาท คงเหลือ', p.stock);
}
# sync สินค้าทั้งร้านเข้าฐานคุณ — ทุกหน้า
products = istatus_get_all("/api/v1/products")
for p in products:
if not p["active"]:
continue # ตัวที่ถูกปิด — ปิดใช้งานฝั่งคุณด้วย
print(p["sku"], p["name"], p["price"] / 100, "บาท คงเหลือ", p["stock"])
// sync สินค้าทั้งร้านเข้าฐานคุณ — ทุกหน้า
var products = await IstatusClient.GetAllAsync("/api/v1/products");
foreach (var p in products.Where(p => p.GetProperty("active").GetBoolean()))
{
Console.WriteLine($"{p.GetProperty("sku").GetString()} {p.GetProperty("name").GetString()} {p.GetProperty("price").GetInt64() / 100.0:F2} บาท");
}
ยอดสต๊อกคงเหลือต่อสินค้า (ค่าเดียวกับที่เห็นบนหน้าเว็บ istatus) เรียงตาม SKU — เฉพาะสินค้าที่เปิดใช้งาน · ไม่มีมูลค่า/ต้นทุน · เหมาะกับงาน sync สต๊อกเป็นรอบ
| พารามิเตอร์ | ชนิด | ความหมาย |
|---|---|---|
sku | string | ดูเฉพาะ SKU นี้ (ตรงตัว) |
cursor | string | ค่า nextCursor จากหน้าก่อน |
limit | integer | จำนวนต่อหน้า 1-100 (ไม่ส่ง = 50) |
{
"rows": [
{ "productId": "7b1c0000-2222-4333-8444-955566667777",
"sku": "A-001", "name": "สินค้าตัวอย่าง", "stock": 42 }
],
"nextCursor": null
}
# เช็ค SKU เดียว
curl -s -G https://istatus.net/api/v1/stock \
-H "Authorization: Bearer $ISTATUS_API_KEY" \
--data-urlencode "sku=A-001"
# สต๊อกทั้งร้าน (ทุกหน้า)
pull_all /api/v1/stock "" | jq -r '[.sku, .stock] | @tsv'
// เช็ค SKU เดียว
$page = istatus_get('/api/v1/stock', ['sku' => 'A-001']);
echo ($page['rows'][0]['stock'] ?? 'ไม่พบ SKU นี้') . "\n";
// สต๊อกทั้งร้าน — ทุกหน้า
$stock = istatus_get_all('/api/v1/stock');
// เช็ค SKU เดียว
const page = await istatusGet('/api/v1/stock', { sku: 'A-001' });
console.log(page.rows[0]?.stock ?? 'ไม่พบ SKU นี้');
// สต๊อกทั้งร้าน — ทุกหน้า
const stock = await istatusGetAll('/api/v1/stock');
# เช็ค SKU เดียว
page = istatus_get("/api/v1/stock", {"sku": "A-001"})
print(page["rows"][0]["stock"] if page["rows"] else "ไม่พบ SKU นี้")
# สต๊อกทั้งร้าน — ทุกหน้า
stock = istatus_get_all("/api/v1/stock")
// เช็ค SKU เดียว
var page = await IstatusClient.GetAsync("/api/v1/stock", new() { ["sku"] = "A-001" });
var rows = page.GetProperty("rows");
Console.WriteLine(rows.GetArrayLength() > 0 ? rows[0].GetProperty("stock").GetInt32().ToString() : "ไม่พบ SKU นี้");
// สต๊อกทั้งร้าน — ทุกหน้า
var stock = await IstatusClient.GetAllAsync("/api/v1/stock");
ลูกค้าที่ร้านบันทึกเอง (ข้อมูลที่ร้านเป็นเจ้าของ) เรียงจากใหม่ไปเก่า — ไม่รวมผู้ซื้อฝั่ง marketplace
| พารามิเตอร์ | ชนิด | ความหมาย |
|---|---|---|
q | string | ค้นหาจากชื่อหรือเบอร์โทร (บางส่วน) |
cursor | string | ค่า nextCursor จากหน้าก่อน |
limit | integer | จำนวนต่อหน้า 1-100 (ไม่ส่ง = 50) |
{
"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
}
# ค้นหาลูกค้า
curl -s -G https://istatus.net/api/v1/customers \
-H "Authorization: Bearer $ISTATUS_API_KEY" \
--data-urlencode "q=สมชาย"
# ลูกค้าทั้งร้าน (ทุกหน้า)
pull_all /api/v1/customers "" | jq -r '[.name, .phone] | @tsv'
// ค้นหาลูกค้าจากเบอร์โทร
$page = istatus_get('/api/v1/customers', ['q' => '0812345678']);
// ลูกค้าทั้งร้าน — ทุกหน้า
$customers = istatus_get_all('/api/v1/customers');
// ค้นหาลูกค้าจากเบอร์โทร
const page = await istatusGet('/api/v1/customers', { q: '0812345678' });
// ลูกค้าทั้งร้าน — ทุกหน้า
const customers = await istatusGetAll('/api/v1/customers');
# ค้นหาลูกค้าจากเบอร์โทร
page = istatus_get("/api/v1/customers", {"q": "0812345678"})
# ลูกค้าทั้งร้าน — ทุกหน้า
customers = istatus_get_all("/api/v1/customers")
// ค้นหาลูกค้าจากเบอร์โทร
var page = await IstatusClient.GetAsync("/api/v1/customers", new() { ["q"] = "0812345678" });
// ลูกค้าทั้งร้าน — ทุกหน้า
var customers = await IstatusClient.GetAllAsync("/api/v1/customers");
ส่งออเดอร์จากระบบของคุณเข้า istatus — บันทึกผ่านเส้นทางเดียวกับการขายหน้าร้าน (ตัดสต๊อก + ลงบัญชีอัตโนมัติ)
404 · ฟิวส์ความถี่ฝั่งเขียน ~2 คำขอ/วินาที/ร้าน
| field | ชนิด | จำเป็น | ความหมาย |
|---|---|---|---|
externalOrderNo | string (≤60) | ใช่ | เลขออเดอร์ในระบบของคุณ — กุญแจกันบันทึกซ้ำ · เลขบิลใน istatus = API-<ค่านี้> |
items[] | array (1-100) | ใช่ | รายการสินค้า: { "sku": "A-001", "qty": 2 } — ห้าม SKU ซ้ำในบิลเดียว (รวม qty มาแทน) |
customerId | uuid | ไม่ | ลูกค้าในร้าน (id จาก GET /api/v1/customers) — ใช้ระดับราคาของลูกค้ารายนั้น |
shipping | integer | ไม่ | ค่าขนส่งที่คิดกับลูกค้า (สตางค์) |
shippingMethod | string | ไม่ | วิธีจัดส่ง |
note | string (≤500) | ไม่ | หมายเหตุบนบิล |
issuedAt | date-time | ไม่ | วันที่เอกสาร (ISO 8601) — ใช้แสดงบนเอกสาร |
trackingNo · carrier | string | ไม่ | เลขพัสดุ / ขนส่ง |
recipientName · recipientPhone · recipientProvince · recipientPostal | string | ไม่ | ข้อมูลผู้รับ (ใช้พิมพ์ใบปะหน้า) |
422 VALIDATION พร้อมบอกชื่อ field ที่เกิน (ไม่ตัดทิ้งเงียบ)
201 — บันทึกเป็นบิลจริงแล้ว
{ "status": "created", "orderNo": "API-PO-2026-0042" }
202 — โควตาออเดอร์ของแพ็กเดือนนี้เต็ม จึงรับเข้าห้องรอ (สำเร็จแล้ว ไม่ใช่ error — ห้ามยิงซ้ำ) ระบบจะบันทึกเข้าให้เองเมื่ออัปเกรดแพ็กหรือขึ้นเดือนใหม่
{ "status": "held", "heldCount": 3 }
200 — เคยส่ง externalOrderNo นี้แล้ว คืนผลเดิม ไม่เกิดบิลซ้ำ
{ "status": "created", "orderNo": "API-PO-2026-0042" }
409 — สต๊อกไม่พอ (ทั้งบิลไม่ถูกบันทึก)
{ "code": "INSUFFICIENT_STOCK", "message": "สต๊อกไม่พอสำหรับ SKU \"A-001\" — เติมสต๊อกแล้วยิงใหม่" }
409 — ห้องรอของร้านเต็ม (5,000 ใบ) ใบนี้ยังไม่ถูกรับ — อัปเกรดแพ็กเพื่อปล่อยคิว แล้วส่งเลขเดิมมาใหม่
{ "code": "HELD_FULL", "message": "ห้องรอของร้านเต็ม (5,000 ใบ) — ..." }
# ส่งออเดอร์เข้า istatus — "PO-2026-0042" คือเลขออเดอร์ในระบบของคุณ (ใช้กันบันทึกซ้ำ)
istatus_send POST /api/v1/orders '{
"externalOrderNo": "PO-2026-0042",
"items": [{"sku": "A-001", "qty": 2}, {"sku": "B-020", "qty": 1}],
"note": "ส่งด่วน"
}'
# ผลลัพธ์: created / held / already — ทั้งสามแบบ = ส่งสำเร็จ ไม่ต้องยิงซ้ำ
# เน็ตหลุดไม่รู้ผล? ยิงซ้ำ payload เดิมได้เลย — เลขเดิม = บิลเดิม ไม่มีทางเกิดบิลซ้ำ
$r = istatus_send('POST', '/api/v1/orders', [
'externalOrderNo' => 'PO-2026-0042', // เลขในระบบคุณ = กุญแจกันบันทึกซ้ำ
'items' => [['sku' => 'A-001', 'qty' => 2]], // ราคาใช้ของแคตตาล็อกในร้าน
'note' => 'ส่งด่วน',
]);
if ($r['status'] === 202) {
// โควตาออเดอร์ของแพ็กเดือนนี้เต็ม → เข้าห้องรอแล้ว ระบบบันทึกให้เองเมื่ออัปแพ็ก/ขึ้นเดือนใหม่
error_log('เข้าห้องรอ — คิวรวม ' . $r['body']['heldCount'] . ' ใบ (ห้ามยิงซ้ำ)');
} else {
echo $r['body']['orderNo']; // API-PO-2026-0042 (201 = สร้างใหม่ · 200 = เคยส่งแล้ว)
}
const r = await istatusSend('POST', '/api/v1/orders', {
externalOrderNo: 'PO-2026-0042', // เลขในระบบคุณ = กุญแจกันบันทึกซ้ำ
items: [{ sku: 'A-001', qty: 2 }], // ราคาใช้ของแคตตาล็อกในร้าน
note: 'ส่งด่วน',
});
if (r.status === 202) {
// โควตาแพ็กเดือนนี้เต็ม → เข้าห้องรอ ระบบบันทึกให้เองเมื่ออัปแพ็ก/ขึ้นเดือนใหม่ (ห้ามยิงซ้ำ)
console.log('เข้าห้องรอ — คิวรวม', r.body.heldCount, 'ใบ');
} else {
console.log('บันทึกแล้ว', r.body.orderNo); // API-PO-2026-0042
}
status, body = istatus_send("POST", "/api/v1/orders", {
"externalOrderNo": "PO-2026-0042", # เลขในระบบคุณ = กุญแจกันบันทึกซ้ำ
"items": [{"sku": "A-001", "qty": 2}], # ราคาใช้ของแคตตาล็อกในร้าน
"note": "ส่งด่วน",
})
if status == 202:
# โควตาแพ็กเดือนนี้เต็ม → เข้าห้องรอ ระบบบันทึกให้เองเมื่ออัปแพ็ก/ขึ้นเดือนใหม่ (ห้ามยิงซ้ำ)
print("เข้าห้องรอ — คิวรวม", body["heldCount"], "ใบ")
else:
print("บันทึกแล้ว", body["orderNo"]) # API-PO-2026-0042
var (status, body) = await IstatusClient.SendAsync(HttpMethod.Post, "/api/v1/orders", new
{
externalOrderNo = "PO-2026-0042", // เลขในระบบคุณ = กุญแจกันบันทึกซ้ำ
items = new[] { new { sku = "A-001", qty = 2 } },
note = "ส่งด่วน",
});
if (status == 202)
// โควตาแพ็กเดือนนี้เต็ม → เข้าห้องรอ ระบบบันทึกให้เองเมื่ออัปแพ็ก/ขึ้นเดือนใหม่ (ห้ามยิงซ้ำ)
Console.WriteLine($"เข้าห้องรอ — คิวรวม {body.GetProperty("heldCount").GetInt32()} ใบ");
else
Console.WriteLine($"บันทึกแล้ว {body.GetProperty("orderNo").GetString()}");
rows = คิวออเดอร์ที่ค้างห้องรอ (โควตาแพ็กเต็ม หรือรอเติมสต๊อก) เรียงตามลำดับที่ระบบจะปล่อยเข้า (100 รายการแรก) —
ไม่ต้องทำอะไรกับคิวนี้ ระบบปล่อยเข้าเองเมื่ออัปเกรดแพ็ก/ขึ้นเดือนใหม่/เติมสต๊อก ·
rejectedRows = ใบที่ระบบลองปล่อยแล้วไม่ผ่านจนครบเพดาน (เช่น SKU ไม่มีในร้าน) — ยังไม่เป็นยอดขาย
แก้ตาม lastError แล้วส่ง externalOrderNo เดิมมาใหม่ (ใหม่สุดก่อน · 100 รายการ)
พารามิเตอร์: ไม่มี
{
"rows": [
{
"id": "7b1c0000-2222-4333-8444-955566667777",
"orderNo": "API-PO-2026-0042",
"status": "held",
"attemptCount": 0, // จำนวนครั้งที่ระบบลองปล่อยแล้ว payload ไม่ผ่าน — โควตาเต็ม/สต๊อกไม่พอ ไม่นับ
"lastError": null, // เหตุผลที่ปล่อยไม่ผ่านครั้งล่าสุด · "insufficient stock: <sku>" = รอเติมสต๊อก
"createdAt": "2026-08-31T10:00:00+07:00"
}
],
"heldCount": 1,
"heldStockCount": 0, // ในจำนวน heldCount มีกี่ใบที่ติดสต๊อกไม่พอ
"rejectedRows": [
{
"id": "7b1c0000-2222-4333-8444-955566668888",
"orderNo": "API-PO-2026-0039",
"status": "rejected",
"attemptCount": 5,
"lastError": "product not found/inactive: OLD-SKU", // แก้ตามนี้แล้วส่ง externalOrderNo เดิมมาใหม่
"createdAt": "2026-08-30T09:00:00+07:00"
}
],
"rejectedCount": 1
}# ดูคิวออเดอร์ที่ค้างห้องรอ (โควตาเต็ม) — เรียงตามลำดับที่ระบบจะปล่อยเข้า
curl -s https://istatus.net/api/v1/orders/held \
-H "Authorization: Bearer $ISTATUS_API_KEY" | jq
$held = istatus_get('/api/v1/orders/held');
echo $held['heldCount'] . ' ใบรอเข้า';
const held = await istatusGet('/api/v1/orders/held');
console.log(held.heldCount, 'ใบรอเข้า');
held = istatus_get("/api/v1/orders/held")
print(held["heldCount"], "ใบรอเข้า")
var held = await IstatusClient.GetAsync("/api/v1/orders/held");
Console.WriteLine($"{held.GetProperty("heldCount").GetInt32()} ใบรอเข้า");
ตั้งยอดคงเหลือของ SKU นี้ (absolute — ไม่ใช่บวก/ลบ) · ระบบออกใบปรับสต๊อกและลงบัญชีให้อัตโนมัติเหมือนกดปรับจากหน้าเว็บ
404) · SKU ที่มี / หรืออักขระพิเศษ ต้อง URL-encode ก่อนใส่ใน path ·
สินค้าจัดชุด (bundle) ปรับตรงไม่ได้ — ยอดคำนวณจากสินค้าฐาน (422 BUNDLE_SKU)
| field | ชนิด | จำเป็น | ความหมาย |
|---|---|---|---|
qty | integer ≥ 0 | ใช่ | ยอดคงเหลือใหม่ |
note | string (≤200) | ไม่ | หมายเหตุบนใบปรับสต๊อก |
{
"status": "ok",
"sku": "A-001",
"stock": 42, // ยอดคงเหลือหลังปรับ
"delta": 5, // ผลต่างที่ปรับ (+เพิ่ม / -ลด / 0 = ยอดตรงอยู่แล้ว)
"adjusted": true, // false = ยอดตรงอยู่แล้ว ระบบไม่สร้างใบเปล่า
"adjustNo": "API-ADJ-1756600000000-123"
}# ตั้งยอดคงเหลือของ A-001 เป็น 42 ชิ้น (absolute ไม่ใช่บวก/ลบ)
istatus_send PUT /api/v1/stock/A-001 '{"qty": 42, "note": "sync จาก ERP"}'
$r = istatus_send('PUT', '/api/v1/stock/' . rawurlencode('A-001'), [
'qty' => 42, // ยอดคงเหลือใหม่ (ไม่ใช่ผลต่าง)
'note' => 'sync จาก ERP',
]);
echo $r['body']['adjusted'] ? "ปรับ {$r['body']['delta']} ชิ้น" : 'ยอดตรงอยู่แล้ว';
const r = await istatusSend('PUT', `/api/v1/stock/${encodeURIComponent('A-001')}`, {
qty: 42, // ยอดคงเหลือใหม่ (ไม่ใช่ผลต่าง)
note: 'sync จาก ERP',
});
console.log(r.body.adjusted ? `ปรับ ${r.body.delta} ชิ้น` : 'ยอดตรงอยู่แล้ว');
from urllib.parse import quote
status, body = istatus_send("PUT", "/api/v1/stock/" + quote("A-001", safe=""),
{"qty": 42, "note": "sync จาก ERP"}) # ยอดคงเหลือใหม่ (ไม่ใช่ผลต่าง)
print(f"ปรับ {body['delta']} ชิ้น" if body["adjusted"] else "ยอดตรงอยู่แล้ว")
var (_, body) = await IstatusClient.SendAsync(HttpMethod.Put,
"/api/v1/stock/" + Uri.EscapeDataString("A-001"),
new { qty = 42, note = "sync จาก ERP" }); // ยอดคงเหลือใหม่ (ไม่ใช่ผลต่าง)
Console.WriteLine(body.GetProperty("adjusted").GetBoolean()
? $"ปรับ {body.GetProperty("delta").GetInt32()} ชิ้น" : "ยอดตรงอยู่แล้ว");
กดปุ่ม Authorize แล้ววาง API key ของร้านคุณ (ไม่ต้องพิมพ์คำว่า Bearer) จากนั้นกด Try it out ที่ endpoint ไหนก็ได้ — เห็นข้อมูลร้านตัวเองจริงก่อนเขียนโค้ดแม้แต่บรรทัดเดียว
กำลังโหลด Swagger UI…