istatus developers

istatus Open API

ให้ ERP / โปรแกรมบัญชี / POS ของคุณดึงข้อมูล ออเดอร์รวมทุกช่องทาง · สินค้า · สต๊อก · ลูกค้า จาก istatus ได้โดยตรงด้วย API key เดียว — ไม่ต้องไปสมัครขอ API กับ Shopee / Lazada / TikTok เอง

Shopee ─┐                                        ┌─ ERP / โปรแกรมบัญชี / POS ของคุณ
Lazada ─┼─ istatus (เชื่อมร้านของคุณอยู่แล้ว) ──┼─ GET /api/v1/...
TikTok ─┤    รวมออเดอร์ / สินค้า / สต๊อก          └─ Authorization: Bearer <API key>
POS ────┘
สิ่งที่ API รุ่นแรก (อ่านอย่างเดียว) ให้ได้ ส่งเข้า (push) — เปิดให้เป็นรายร้าน

ฝั่งเขียนเปิดให้เป็นรายร้าน — ถ้าร้านคุณยังไม่ได้เปิด เส้นเขียนจะตอบ 404 (ทักเราเพื่อขอเปิด)

สำหรับผู้ใช้ AI ช่วยเขียนโค้ด: โยนหน้านี้ทั้งหน้า (หรือ /api/v1/openapi.json) ให้ AI อ่าน แล้วสั่งว่า "เขียนโค้ดดึงออเดอร์จาก istatus Open API ตามเอกสารนี้" ได้เลย — เอกสารนี้มีตัวอย่าง request/response จริงและกติกาครบทุกข้อ

เริ่มต้นเร็ว (5 นาที)

  1. ออก API key: เข้าแอป istatus ด้วยบัญชีเจ้าของร้าน → เมนู ตั้งค่า → หัวข้อ "เชื่อมต่อ API / ERP" → กด ออก API key
    key ขึ้นต้นด้วย ist_ และแสดงเต็มครั้งเดียว — คัดลอกเก็บทันที (ระบบเก็บเป็นรหัสย่อยทางเดียว ดูซ้ำไม่ได้ · ออกได้สูงสุด 3 ใบต่อร้าน)
  2. เก็บ key ใน environment variable ของเครื่องที่จะเรียกใช้ — ห้ามฝังในโค้ดหรือคอมมิตลง git
    export ISTATUS_API_KEY="ist_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  3. ทดสอบยิงครั้งแรก:
    curl -s https://istatus.net/api/v1/ping \
      -H "Authorization: Bearer $ISTATUS_API_KEY"
    ถ้า key ใช้ได้จะตอบ:
    { "ok": true, "plan": "pro", "trialDaysLeft": null, "shopName": "ร้านของคุณ" }
  4. ดึงออเดอร์จริง:
    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 ของร้าน>

กติกาการใช้ (อ่านก่อนเขียนโค้ด)

สิทธิ์ใช้ API — แพ็ก Pro ขึ้นไป / ทดลองฟรี 30 วัน

ฟิวส์ความถี่ — 429 + Retry-After

Pagination — cursor (ทุก endpoint ที่เป็นรายการ)

หน่วยเงินและตัวเลข (สำคัญมาก)

ส่งข้อมูลเข้า (write) — กติกาที่ต้องรู้ก่อนเขียนโค้ด push

ข้อจำกัดข้อมูล marketplace (นโยบายแพลตฟอร์ม)

รูปแบบ error (เหมือนกันทุก endpoint)

{ "code": "RATE_LIMITED", "message": "ยิงถี่เกินกำหนด — ดู header Retry-After แล้วลองใหม่" }

ให้โค้ดของคุณตัดสินใจจาก code (คงที่ เครื่องอ่านได้) — message เป็นภาษาไทยสำหรับคน อาจเปลี่ยนถ้อยคำได้

HTTPcodeความหมายโค้ดของคุณควรทำ
401INVALID_KEYkey ผิด / ถูกเพิกถอน / ไม่ได้ส่งหยุด — ตรวจ key ใน env / ออกใบใหม่
403PLAN_REQUIREDแพ็กยังไม่ถึง Pro และไม่อยู่ในช่วงทดลองหยุด — อัปเกรดแพ็กในแอป istatus
403TRIAL_EXPIREDช่วงทดลอง 30 วันหมดแล้วหยุด — อัปเกรดแพ็ก (key เดิมใช้ต่อได้)
422INVALID_PARAMพารามิเตอร์ผิดรูปแบบ (เช่น from ไม่ใช่ YYYY-MM-DD)แก้พารามิเตอร์ — อย่ายิงซ้ำแบบเดิม
422INVALID_CURSORcursor ไม่ใช่ค่าที่ระบบให้เริ่มดึงใหม่จากหน้าแรก (ไม่ส่ง cursor)
429RATE_LIMITEDยิงถี่เกินฟิวส์ต่อร้านรอตาม header Retry-After แล้วยิงใหม่
404ร้านนี้ยังไม่ได้เปิดฝั่งเขียน (เส้น POST/PUT/orders/held)หยุด — ทักทีมงานเพื่อขอเปิด
404NOT_FOUNDไม่พบ SKU ที่ระบุใน PUT /stock/{sku}ตรวจ SKU กับ GET /api/v1/products
409INSUFFICIENT_STOCKสต๊อกไม่พอ — ทั้งบิลไม่ถูกบันทึกเติมสต๊อกแล้วยิงใหม่ (เลขเดิมได้ ไม่ซ้ำ)
409HELD_FULLห้องรอของร้านเต็ม (5,000 ใบ) — ใบนี้ยังไม่ถูกรับหยุด — แจ้งเจ้าของร้านอัปเกรดแพ็กเพื่อปล่อยคิว แล้วส่งเลขเดิมใหม่
409STALE_STOCKPUT /stock: ยอดจริงไม่ตรง expectedQty (มีการขาย/ปรับระหว่างนั้น)อ่านยอดใหม่จาก GET /stock แล้วส่งอีกครั้ง — ไม่มีอะไรถูกแตะ
409IN_PROGRESSPOST /orders: เลขนี้กำลังถูกบันทึกโดยคำขออื่นพร้อมกัน (เช่น ยิงซ้ำเร็วเกินไป)รอ 2 วินาทีแล้วส่งเลขเดิมซ้ำ — จะได้ผลจริง (created / held / voided)
503RETRYเลขใบปรับสต๊อกชนกัน (พบยากมาก) — ยอดยังไม่ถูกแตะยิง PUT /stock ใบเดิมซ้ำได้ทันที
422VALIDATIONbody ผิดรูปแบบ / มี field ที่ไม่รู้จักแก้ body ตามข้อความ — อย่ายิงซ้ำแบบเดิม
422UNKNOWN_SKUไม่มี SKU นี้ในร้าน (หรือถูกปิดใช้งาน)สร้าง/เปิดสินค้าในแอปก่อน แล้วยิงใหม่
422BUNDLE_SKUตั้งสต๊อกให้สินค้าจัดชุดโดยตรงไม่ได้ (ยอดคำนวณจากสินค้าฐาน)ตั้งยอดที่สินค้าฐานแทน
500INTERNALข้อผิดพลาดฝั่งระบบรอครู่แล้วลองใหม่ — ถ้าค้างนานติดต่อเรา
สรุปฝั่งเขียน: 201 = บันทึกแล้ว · 202 = เข้าห้องรอ (สำเร็จ ไม่ต้องยิงซ้ำ) · 200 = เลขนี้เคยส่งแล้ว ได้ผลเดิม — ทั้งสามอย่างนี้ถือว่า "ส่งสำเร็จ" ในโค้ดของคุณ

โค้ดสำเร็จรูป — โครงหลัก 5 ภาษา (copy ไปใช้ได้เลย)

ทุกภาษาเป็นโครงเดียวกัน: อ่าน 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;
    }
}

โครงฝั่งเขียน (push) — 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 ครั้งแล้วยังติดฟิวส์ — ลดความถี่การเรียก");
}

อ้างอิง API (ทุก endpoint)

Base URL: https://istatus.net · ทุกเส้นต้องแนบ Authorization: Bearer · เส้นอ่านเป็น GET · เส้นเขียน (POST /orders, PUT /stock/{sku}) เปิดให้เป็นรายร้าน — ปิดอยู่ = 404 · ทุก response เป็น JSON (UTF-8) · error ที่เป็นไปได้ทุกเส้น: 401 / 403 / 422 / 429 (ดูตาราง error)

GET/api/v1/ping

เช็คว่า key ใช้ได้ + แพ็กปัจจุบัน + วันทดลองคงเหลือ — ใช้เป็นคำขอแรกตอนตั้งค่าระบบ และใช้เช็คสุขภาพการเชื่อมต่อ

พารามิเตอร์: ไม่มี

ตัวอย่าง response 200

{
  "ok": true,
  "plan": "starter",
  "trialDaysLeft": 22,
  "shopName": "ร้านตัวอย่าง"
}
fieldชนิดความหมาย
okbooleantrue เสมอเมื่อมาถึงขั้นนี้ (key + สิทธิ์ผ่านแล้ว)
planstringแพ็กปัจจุบัน: free | starter | pro | business | scale
trialDaysLeftinteger | nullวันทดลองที่เหลือ — null = แพ็ก Pro ขึ้นไป (ไม่ใช่โหมดทดลอง)
shopNamestring | 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());

GET/api/v1/orders

รายการออเดอร์รวมทุกช่องทาง (POS / Shopee / Lazada / TikTok / WooCommerce ฯลฯ) เรียงจากใหม่ไปเก่า — แต่ละออเดอร์แนบ items[] (รายการสินค้าในบิล) มาให้ครบในคำขอเดียว

พารามิเตอร์ชนิดความหมาย
fromdateวันที่เริ่ม YYYY-MM-DD (นับตั้งแต่ต้นวัน)
todateวันที่สิ้นสุด YYYY-MM-DD (รวมทั้งวัน)
channelstringกรองช่องทาง: pos, shopee, lazada, tiktok, woocommerce, web
channel=apistringเฉพาะออเดอร์ที่ระบบของคุณส่งเข้ามาเองผ่าน POST /orders — ทุกแถวมี field origin (api / pos / shopee …) ให้แยก "ของที่ ERP ส่ง" ออกจากขายหน้าร้านจริง กัน sync กลับซ้ำ
statusstringกรองสถานะ เช่น paid (ขายแล้ว), voided (ยกเลิกบิล)
cursorstringค่า nextCursor จากหน้าก่อน
limitintegerจำนวนต่อหน้า 1-100 (ไม่ส่ง = 50)
กรอง channel เป็นช่อง marketplace = ข้อมูลย้อนหลังไม่เกิน 90 วัน (response มี mpLimited: true) — ไม่กรอง channel หรือกรอง pos ไม่ติดข้อจำกัดนี้

ตัวอย่าง response 200

{
  "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} บาท");
}

GET/api/v1/products

รายการสินค้า + ราคาขาย เรียงตาม SKU (A→Z) — คืนทั้งสินค้าที่เปิดและปิดใช้งานพร้อม flag active (ระบบที่ sync ต้องเห็นตัวที่ถูกปิดด้วย จะได้ไม่ค้างในระบบคุณ) · ไม่มีข้อมูลต้นทุน

พารามิเตอร์ชนิดความหมาย
qstringค้นหาจาก SKU / ชื่อสินค้า (บางส่วน) หรือบาร์โค้ด (ตรงตัว)
cursorstringค่า nextCursor จากหน้าก่อน
limitintegerจำนวนต่อหน้า 1-100 (ไม่ส่ง = 50)

ตัวอย่าง response 200

{
  "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} บาท");
}

GET/api/v1/stock

ยอดสต๊อกคงเหลือต่อสินค้า (ค่าเดียวกับที่เห็นบนหน้าเว็บ istatus) เรียงตาม SKU — เฉพาะสินค้าที่เปิดใช้งาน · ไม่มีมูลค่า/ต้นทุน · เหมาะกับงาน sync สต๊อกเป็นรอบ

พารามิเตอร์ชนิดความหมาย
skustringดูเฉพาะ SKU นี้ (ตรงตัว)
cursorstringค่า nextCursor จากหน้าก่อน
limitintegerจำนวนต่อหน้า 1-100 (ไม่ส่ง = 50)

ตัวอย่าง response 200

{
  "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");

GET/api/v1/customers

ลูกค้าที่ร้านบันทึกเอง (ข้อมูลที่ร้านเป็นเจ้าของ) เรียงจากใหม่ไปเก่า — ไม่รวมผู้ซื้อฝั่ง marketplace

พารามิเตอร์ชนิดความหมาย
qstringค้นหาจากชื่อหรือเบอร์โทร (บางส่วน)
cursorstringค่า nextCursor จากหน้าก่อน
limitintegerจำนวนต่อหน้า 1-100 (ไม่ส่ง = 50)

ตัวอย่าง response 200

{
  "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");

POST/api/v1/orders

ส่งออเดอร์จากระบบของคุณเข้า istatus — บันทึกผ่านเส้นทางเดียวกับการขายหน้าร้าน (ตัดสต๊อก + ลงบัญชีอัตโนมัติ)

เส้นนี้เปิดให้เป็นรายร้าน — ร้านที่ยังไม่ได้เปิดจะได้ 404 · ฟิวส์ความถี่ฝั่งเขียน ~2 คำขอ/วินาที/ร้าน

body (JSON)

fieldชนิดจำเป็นความหมาย
externalOrderNostring (≤60)ใช่เลขออเดอร์ในระบบของคุณ — กุญแจกันบันทึกซ้ำ · เลขบิลใน istatus = API-<ค่านี้>
items[]array (1-100)ใช่รายการสินค้า: { "sku": "A-001", "qty": 2 }ห้าม SKU ซ้ำในบิลเดียว (รวม qty มาแทน)
customerIduuidไม่ลูกค้าในร้าน (id จาก GET /api/v1/customers) — ใช้ระดับราคาของลูกค้ารายนั้น
shippingintegerไม่ค่าขนส่งที่คิดกับลูกค้า (สตางค์)
shippingMethodstringไม่วิธีจัดส่ง
notestring (≤500)ไม่หมายเหตุบนบิล
issuedAtdate-timeไม่วันที่เอกสาร (ISO 8601) — ใช้แสดงบนเอกสาร
trackingNo · carrierstringไม่เลขพัสดุ / ขนส่ง
recipientName · recipientPhone · recipientProvince · recipientPostalstringไม่ข้อมูลผู้รับ (ใช้พิมพ์ใบปะหน้า)
ไม่มีช่องราคา/ส่วนลดโดยตั้งใจ — ระบบคิดราคาจากราคาขายในแคตตาล็อกของร้านเสมอ (กันราคาเพี้ยนจากระบบภายนอก) · ส่ง field ที่ไม่อยู่ในตารางนี้มา = 422 VALIDATION พร้อมบอกชื่อ field ที่เกิน (ไม่ตัดทิ้งเงียบ)

ตัวอย่าง response

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()}");

GET/api/v1/orders/held

rows = คิวออเดอร์ที่ค้างห้องรอ (โควตาแพ็กเต็ม หรือรอเติมสต๊อก) เรียงตามลำดับที่ระบบจะปล่อยเข้า (100 รายการแรก) — ไม่ต้องทำอะไรกับคิวนี้ ระบบปล่อยเข้าเองเมื่ออัปเกรดแพ็ก/ขึ้นเดือนใหม่/เติมสต๊อก · rejectedRows = ใบที่ระบบลองปล่อยแล้วไม่ผ่านจนครบเพดาน (เช่น SKU ไม่มีในร้าน) — ยังไม่เป็นยอดขาย แก้ตาม lastError แล้วส่ง externalOrderNo เดิมมาใหม่ (ใหม่สุดก่อน · 100 รายการ)

พารามิเตอร์: ไม่มี

ตัวอย่าง response 200

{
  "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()} ใบรอเข้า");

PUT/api/v1/stock/{sku}

ตั้งยอดคงเหลือของ SKU นี้ (absolute — ไม่ใช่บวก/ลบ) · ระบบออกใบปรับสต๊อกและลงบัญชีให้อัตโนมัติเหมือนกดปรับจากหน้าเว็บ

เส้นนี้เปิดให้เป็นรายร้าน (ปิดอยู่ = 404) · SKU ที่มี / หรืออักขระพิเศษ ต้อง URL-encode ก่อนใส่ใน path · สินค้าจัดชุด (bundle) ปรับตรงไม่ได้ — ยอดคำนวณจากสินค้าฐาน (422 BUNDLE_SKU)

body (JSON)

fieldชนิดจำเป็นความหมาย
qtyinteger ≥ 0ใช่ยอดคงเหลือใหม่
notestring (≤200)ไม่หมายเหตุบนใบปรับสต๊อก

ตัวอย่าง response 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()} ชิ้น" : "ยอดตรงอยู่แล้ว");

ลองยิงจริง (Swagger UI)

กดปุ่ม Authorize แล้ววาง API key ของร้านคุณ (ไม่ต้องพิมพ์คำว่า Bearer) จากนั้นกด Try it out ที่ endpoint ไหนก็ได้ — เห็นข้อมูลร้านตัวเองจริงก่อนเขียนโค้ดแม้แต่บรรทัดเดียว

key ที่วางในหน้านี้ใช้เฉพาะในหน้าต่างนี้ ไม่ถูกบันทึกลงเครื่อง/เซิร์ฟเวอร์ — ปิดหน้าแล้วหายทันที · สเปกเครื่องอ่านได้ (OpenAPI 3.0): /api/v1/openapi.json

กำลังโหลด Swagger UI…