Sellstory APIOpen API v1 Reference
v1.1.0
REST API · JSON

Sellstory Open API v1

Complete integration reference for the Customer & Product APIs — authentication, endpoints, request/response schemas, error handling, and code examples.

Version 1.1.0 Updated 25 Nov 2025 21 endpoints Customer + Company & Product with Algolia sync

Authentication

Base URL: https://openapi.sellstory.me/ — ทุก endpoint path ในเอกสารนี้ (เช่น /api/v1/auth/token) ให้ต่อท้าย base URL นี้เมื่อเรียกใช้งานจริง

1. Request API Credentials

ขอ API Key และ API Secret สำหรับ workspace ของคุณ

POST /api/v1/auth/request-credentials

Request Body

Field Type Required Description
workspaceId string บังคับ รหัส Workspace ของคุณ
requestedBy string บังคับ Email ของผู้ขอ API Key
purpose string บังคับ วัตถุประสงค์ในการใช้งาน API
environment string ไม่บังคับ production หรือ sandbox

Example Request

{
  "workspaceId": "your-workspace-id",
  "requestedBy": "your-email@company.com",
  "purpose": "Integration with CRM System",
  "environment": "production"
}

Example Response — 201 Created

{
  "success": true,
  "data": {
    "apiKey": "sk_live_xxxxxxxxxxxxxxxx",
    "apiSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "environment": "production",
    "scopes": ["customer:read", "customer:write", "product:read", "product:write", "card:read", "card:write", "card:delete", "board:read", "lane:read", "member:read"],
    "rateLimit": 100,
    "createdAt": "2025-11-06T07:00:00.000Z"
  },
  "requestId": "req_xxxxxxxxxxxxx"
}
สำคัญ! apiSecret จะแสดงเพียงครั้งเดียว กรุณาเก็บไว้อย่างปลอดภัย!

2. Generate JWT Token

สร้าง JWT Token สำหรับใช้เรียก API อื่นๆ

POST /api/v1/auth/token

Request Body

Field Type Required Description
apiKey string บังคับ API Key ที่ได้จากขั้นตอนที่ 1
apiSecret string บังคับ API Secret ที่ได้จากขั้นตอนที่ 1

Example Response — 200 OK

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "scope": "customer:read customer:write product:read product:write"
  },
  "requestId": "req_xxxxxxxxxxxxx"
}
Token Expiration: JWT Token หมดอายุใน 1 ชั่วโมง (3600 วินาที)

Customer APIs

3. Create/Update Customer (Upsert)

สร้างหรืออัปเดตข้อมูลลูกค้า (ระบบจะค้นหาด้วย customId หรือ email ถ้ามีอยู่แล้วจะอัปเดต ถ้าไม่มีจะสร้างใหม่)

POST /api/v1/customers/upsert
Auto Company Creation: ถ้าส่ง company object มาด้วย ระบบจะ:
  • สร้าง/อัปเดต Company อัตโนมัติ
  • Link Customer กับ Company

Headers

Authorization: Bearer {accessToken}
Content-Type: application/json

Required Fields (ฟิลด์บังคับ)

Field Type Description
name string ชื่อ-นามสกุล ลูกค้า

Optional Fields (ฟิลด์ไม่บังคับ)

Field Type Description Example
customId string รหัสลูกค้าจากระบบของคุณ "CUST-001"
email string อีเมล "john@example.com"
phone string เบอร์โทรศัพท์ "0812345678"
customerType string ประเภท: Customer, Lead, Prospect "Customer"
gender string เพศ: Male, Female, Other "Male"
source string แหล่งที่มา "Facebook Ads"
tags string[] แท็ก ["vip", "wholesale"]
address object ที่อยู่ลูกค้า (object format) { addressLine1, subdistrict, district, province, postalCode }
addressLine1 string ที่อยู่ (flat format - legacy) "123 ถนนสุขุมวิท"
company object Auto-create Company! ถ้าส่งมา จะสร้างบริษัทและ link กัน ดูตัวอย่างด้านล่าง
customFields object ฟิลด์เพิ่มเติม {"หมายเหตุ": "VIP"}

Company Object Fields (สำหรับ auto-create company)

Field Type Description Fallback
customId string รหัสบริษัทจากระบบของคุณ (ใช้ค้นหา company ที่มีอยู่) -
name string บังคับ ชื่อบริษัท -
taxId string เลขประจำตัวผู้เสียภาษี (ใช้ค้นหา company ที่มีอยู่) -
branch string สาขา "สำนักงานใหญ่"
phone string เบอร์บริษัท ใช้ phone ของ customer
email string อีเมลบริษัท ใช้ email ของ customer
website string เว็บไซต์ ใช้ website ของ customer
address string ที่อยู่บริษัท ใช้ address ของ customer
addressLine1 string บ้านเลขที่/อาคาร ใช้จาก customer
subdistrict string ตำบล/แขวง ใช้จาก customer
district string อำเภอ/เขต ใช้จาก customer
province string จังหวัด ใช้จาก customer
postalCode string รหัสไปรษณีย์ ใช้จาก customer

Example Request (Customer Only)

{
  "customId": "CUST-001",
  "name": "สมชาย ใจดี",
  "email": "somchai@example.com",
  "phone": "0812345678",
  "customerType": "Customer",
  "gender": "Male",
  "source": "Facebook",
  "tags": ["vip", "wholesale"],
  "address": {
    "addressLine1": "123 ถนนสุขุมวิท",
    "subdistrict": "คลองเตย",
    "district": "คลองเตย",
    "province": "กรุงเทพมหานคร",
    "postalCode": "10110"
  },
  "customFields": {
    "ประเภทธุรกิจ": "ค้าส่ง",
    "วงเงินเครดิต": "100000"
  }
}

Example Request (Customer + Company)

Auto Company Features:
  • ถ้า Company มี customId, taxId หรือ name ตรงกับที่มีอยู่ → อัปเดต
  • ถ้าไม่มี → สร้าง Company ใหม่
  • ฟิลด์ที่ไม่ได้ส่งใน company → ใช้ข้อมูลจาก customer แทน
  • Response จะมีทั้ง data (customer) และ company
{
  "customId": "CUST-001",
  "name": "สมชาย ใจดี",
  "email": "somchai@example.com",
  "phone": "0812345678",
  "customerType": "Customer",
  "gender": "Male",
  "source": "Facebook",
  "tags": ["vip", "wholesale"],
  "address": {
    "addressLine1": "123 ถนนสุขุมวิท",
    "subdistrict": "คลองเตย",
    "district": "คลองเตย",
    "province": "กรุงเทพมหานคร",
    "postalCode": "10110"
  },
  "company": {
    "customId": "COMP-001",
    "name": "บริษัท ซื้อมาก จำกัด",
    "taxId": "0123456789012",
    "branch": "สำนักงานใหญ่"
  },
  "customFields": {
    "ประเภทธุรกิจ": "ค้าส่ง",
    "วงเงินเครดิต": "100000"
  }
}

Note: ในตัวอย่างนี้ company ไม่ได้ส่ง phone, email, address มา ระบบจะใช้ข้อมูลจาก customer (somchai@example.com, 0812345678, ที่อยู่คลองเตย) แทนอัตโนมัติ

Response — 201 Created

{
  "success": true,
  "data": {
    "action": "created",
    "data": {
      "id": "abc123xyz",
      "customId": "CUST-001",
      "name": "สมชาย ใจดี",
      "email": "somchai@example.com",
      "customerType": "Customer",
      "companyNames": [{
        "id": "company456",
        "label": "สำนักงานใหญ่",
        "value": "บริษัท ซื้อมาก จำกัด"
      }],
      "workspaceId": "your-workspace-id",
      "createdAt": 1699257600000,
      "updatedAt": 1699257600000
    },
    "company": {
      "id": "company456",
      "name": "บริษัท ซื้อมาก จำกัด",
      "taxId": "0123456789012",
      "branch": "สำนักงานใหญ่",
      "emails": [{"value": "somchai@example.com"}],
      "phones": [{"value": "0812345678"}],
      "associatedCustomerIds": ["abc123xyz"],
      "createdAt": 1699257600000
    }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

Response — 200 OK (Updated)

{
  "success": true,
  "data": {
    "action": "updated",
    "customer": {
      "id": "abc123xyz",
      "customId": "CUST-001",
      "name": "สมชาย ใจดี (แก้ไข)",
      "email": "somchai@example.com",
      "updatedAt": 1699257700000
    }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

4. Get Customer by ID

GET /api/v1/customers/{id}

5. Get Customer by Custom ID

GET /api/v1/customers/customId/{customId}

6. List Customers

GET /api/v1/customers?page=1&limit=10

Query Parameters (ทั้งหมดไม่บังคับ)

Parameter Type Description Default
page number หน้าที่ต้องการ 1
limit number จำนวนรายการต่อหน้า (สูงสุด 100) 20
search string คำค้นหา (ชื่อ, อีเมล, เบอร์โทร) -
customerType string กรองตามประเภท -
sortBy string เรียงลำดับตามฟิลด์ updatedAt
sortOrder string asc หรือ desc desc

Response Headers (Rate Limiting)

Header Description Example
X-RateLimit-Limit จำนวน requests สูงสุดต่อนาที 100
X-RateLimit-Remaining จำนวน requests ที่เหลือ 95
X-RateLimit-Reset เวลาที่รีเซ็ต (Unix timestamp) 1699257660

Product APIs

7. Create/Update Product (Upsert)

สร้างหรืออัปเดตข้อมูลสินค้า (ระบบจะค้นหาด้วย customId, sku หรือ barcode ถ้ามีอยู่แล้วจะอัปเดต ถ้าไม่มีจะสร้างใหม่)

POST /api/v1/products/upsert
Upsert Priority: ระบบจะค้นหาตามลำดับ 1) customId 2) sku 3) barcode

Headers

Authorization: Bearer {accessToken}
Content-Type: application/json

Required Fields (ฟิลด์บังคับ)

Field Type Description
name string ชื่อสินค้า
price number ราคาขาย (ต้อง >= 0)

Optional Fields (ฟิลด์ไม่บังคับ)

Field Type Description Example
customId string รหัสสินค้าจากระบบของคุณ (ใช้สำหรับ upsert) "PROD-001"
sku string รหัสสินค้า (Stock Keeping Unit) "SKU-001"
barcode string บาร์โค้ด "8850999320014"
description string รายละเอียดสินค้า "สินค้าคุณภาพดี"
category string หมวดหมู่สินค้า "อิเล็กทรอนิกส์"
costPrice number ราคาทุน 150
unit string หน่วยนับ "ชิ้น", "กล่อง"
initialStock number สต็อกเริ่มต้น (สำหรับสินค้าใหม่) 100
availableStock number สต็อกที่มี (สำหรับอัปเดต) 50
reorderLevel number จุดสั่งซื้อใหม่ 10
targetStockLevel number สต็อกเป้าหมาย 100
supplier string ผู้จัดจำหน่าย "ABC Supply Co."
status string สถานะ: active, draft, discontinued "active"
imageUrl string URL รูปภาพหลัก "https://..."
imageSet string[] รูปภาพเพิ่มเติม ["https://...", "https://..."]
showInCatalog boolean แสดงในแค็ตตาล็อก true
hashtags string[] แฮชแท็ก ["ขายดี", "โปรโมชั่น"]
customFields object ฟิลด์เพิ่มเติม {"วันหมดอายุ": "2025-12-31"}
Stock Management: สำหรับสินค้าใหม่ ระบบจะใช้ initialStock เป็นค่าเริ่มต้นทั้ง initialStock และ availableStock หากไม่ระบุจะเป็น 0

Example Request

{
  "sku": "SKU-001",
  "barcode": "8850999320014",
  "name": "เสื้อยืดคอกลม สีขาว",
  "description": "เสื้อยืดผ้าคอตตอน 100% สีขาว ใส่สบาย",
  "price": 299,
  "costPrice": 150,
  "category": "เสื้อผ้า",
  "unit": "ตัว",
  "initialStock": 100,
  "reorderLevel": 10,
  "targetStockLevel": 100,
  "supplier": "ABC Textile Co.",
  "status": "active",
  "imageUrl": "https://example.com/shirt-white.jpg",
  "imageSet": [
    "https://example.com/shirt-white-front.jpg",
    "https://example.com/shirt-white-back.jpg"
  ],
  "showInCatalog": true,
  "hashtags": ["เสื้อผ้า", "ขายดี", "basic"],
  "customFields": {
    "แบรนด์": "MyBrand",
    "ประเทศผลิต": "ไทย"
  }
}

Response — 201 Created

{
  "success": true,
  "data": {
    "action": "created",
    "product": {
      "id": "prod123xyz",
      "sku": "SKU-001",
      "sku_lower": "sku-001",
      "barcode": "8850999320014",
      "name": "เสื้อยืดคอกลม สีขาว",
      "price": 299,
      "initialStock": 100,
      "availableStock": 100,
      "workspaceId": "your-workspace-id",
      "createdAt": 1699257600000,
      "updatedAt": 1699257600000
    }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

Response — 200 OK (Updated)

{
  "success": true,
  "data": {
    "action": "updated",
    "product": {
      "id": "prod123xyz",
      "sku": "SKU-001",
      "name": "เสื้อยืดคอกลม สีขาว (อัปเดต)",
      "price": 349,
      "availableStock": 75,
      "updatedAt": 1699257700000
    }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

8. Get Product by ID

GET /api/v1/products/{id}

ดึงข้อมูลสินค้าด้วย Firestore Document ID

9. Get Product by Custom ID

GET /api/v1/products/customId/{customId}

ดึงข้อมูลสินค้าด้วยรหัสสินค้าจากระบบของคุณ

10. Get Product by SKU

GET /api/v1/products/sku/{sku}

ดึงข้อมูลสินค้าด้วยรหัส SKU (ไม่สนใจตัวพิมพ์เล็ก-ใหญ่)

11. Get Product by Barcode

GET /api/v1/products/barcode/{barcode}

ดึงข้อมูลสินค้าด้วยบาร์โค้ด

12. List Products

GET /api/v1/products?page=1&limit=20

Query Parameters (ทั้งหมดไม่บังคับ)

Parameter Type Description Default
page number หน้าที่ต้องการ 1
limit number จำนวนรายการต่อหน้า (สูงสุด 100) 20
search string คำค้นหา (ชื่อ, รายละเอียด, SKU, บาร์โค้ด) -
category string กรองตามหมวดหมู่ -
status string active, draft, discontinued -
minPrice number ราคาต่ำสุด -
maxPrice number ราคาสูงสุด -
inStock boolean กรองเฉพาะสินค้าในสต็อก -
hashtags string แฮชแท็ก (คั่นด้วย comma) -
sortBy string เรียงลำดับตามฟิลด์ (name, price, updatedAt) updatedAt
sortOrder string asc หรือ desc desc

Example Request

GET /api/v1/products?page=1&limit=10&category=เสื้อผ้า&inStock=true&sortBy=price&sortOrder=asc
Authorization: Bearer {accessToken}

Example Response — 200 OK

{
  "success": true,
  "data": {
    "products": [
      {
        "id": "prod123xyz",
        "sku": "SKU-001",
        "barcode": "8850999320014",
        "name": "เสื้อยืดคอกลม สีขาว",
        "price": 299,
        "availableStock": 75,
        "category": "เสื้อผ้า",
        "status": "active"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "totalPages": 1,
      "hasMore": false
    }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

JobCard APIs

ต่างจาก Customer/Product: JobCard ใช้ REST verbs ชัดเจน (POST / PATCH / DELETE by id) ไม่ใช่ upsert เพราะ customId ถูก generate โดยระบบ และการ์ดถูกอ้างอิงด้วย id. ทุก id ที่ต้องใช้ (boardId, laneId, assignee, custom field) ดูได้จาก Lookups

Create JobCard

สร้าง Job Card ใหม่บน board + lane ที่ระบุ ระบบจะ generate id + customId ให้

POST /api/v1/jobcards
Provenance อัตโนมัติ: การ์ดที่สร้างผ่าน API จะถูกติดแท็ก source: "open-api" และ origin: { channel, apiKeyId, clientName } ไว้ filter ภายหลัง; createdBy = actorUserId ที่ส่งมา (ถ้าไม่ส่ง = api:<apiKeyId>)

Headers

Authorization: Bearer {accessToken}
Content-Type: application/json

Required Fields (ฟิลด์บังคับ)

Field Type Description Source
boardId string บอร์ดที่การ์ดจะไปอยู่ GET /boards
laneId string เลน (ต้องอยู่ใน board เดียวกัน) GET /boards/{boardId}/lanes
title string ชื่อ Job Card -

Key Optional Fields (ฟิลด์ไม่บังคับที่ใช้บ่อย)

Field Type Description Source
externalId string คีย์ของคุณเองไว้กันยิงซ้ำ (idempotency) -
actorUserId string UID ผู้ใช้จริง → ลง createdBy/assignedTo GET /members
customerId string ลูกค้าที่ผูกกับการ์ด GET /customers
assignedTo / watchers / collaborators string / string[] ผู้รับผิดชอบ / ผู้ติดตาม (UID) GET /members
status string Pending, In Progress, Done, Cancelled, Archived (default Pending) -
priority string Low, Medium, High, Urgent (default Medium) -
items object[] รายการสินค้า (productId optional) GET /products
expenses, todos, notes, hashtags array ค่าใช้จ่าย / งานย่อย / โน้ต / แท็ก -
customFields object[] ค่า custom field key ด้วย templateFieldUid ระบบ validate ตาม schema ของ board GET /boards/{boardId}/custom-fields
skipAutomation boolean true = ไม่รัน board automation (ใช้ตอน bulk import) -
Read-only / ไม่รับใน v1: customId, order, createdBy, participantIds ฯลฯ ระบบกำหนดเอง (ส่งมาจะถูก ignore). ฟิลด์กลุ่ม parentJobCardRef, childJobCardRefs, recurring, shop-order, template ids → ส่งมาจะได้ 400

Example Request

{
  "boardId": "board_abc",
  "laneId": "lane_def",
  "title": "ติดตั้งแอร์ 2 เครื่อง",
  "externalId": "crm-sync-9931",
  "actorUserId": "uid_seller_7",
  "customerId": "cust_123",
  "assignedTo": "uid_seller_7",
  "priority": "High",
  "status": "Pending",
  "items": [
    { "productId": "prod_air12", "name": "แอร์ 12000 BTU", "quantity": 2, "pricePerUnit": 15000 }
  ],
  "customFields": [
    { "templateFieldUid": "fld_area", "value": "ห้องนอนชั้น 2" }
  ]
}

Response: 201 Created

{
  "success": true,
  "data": {
    "id": "card_XYZ",
    "customId": "SALE-001",
    "boardId": "board_abc",
    "laneId": "lane_def",
    "title": "ติดตั้งแอร์ 2 เครื่อง",
    "status": "Pending",
    "createdBy": "uid_seller_7",
    "source": "open-api",
    "origin": { "channel": "open-api", "apiKeyId": "sk_test_xxx", "clientName": "CRM Sync" },
    "externalId": "crm-sync-9931",
    "createdAt": 1699257600000,
    "updatedAt": 1699257600000
  },
  "requestId": "req_xxxxxxxxxxxxx"
}
Idempotency: ส่ง externalId เดิมซ้ำ → คืนการ์ดเดิม (200) ไม่สร้างซ้ำ

Get JobCard by ID

GET /api/v1/jobcards/{id}

คืน card เต็มทุกฟิลด์

List JobCards

GET /api/v1/jobcards?boardId=...&source=open-api

คืนแบบ slim + pagination (ฟิลด์เต็มดูที่ /jobcards/{id})

Query Parameters (ทั้งหมดไม่บังคับ)

Parameter Description
boardId, laneId, status, customerId, assignedTo กรองตามฟิลด์นั้น
source, originApiKeyId, originClient กรอง "การ์ดที่สร้างจาก Open API" / เจาะจง API key / integration
search ค้นหา (title, customId, description)
createdFrom, createdTo ช่วงวันที่ (Unix ms)
page, limit, sortBy, sortOrder page (default 1), limit (≤100), sortBy (updatedAt/createdAt/order/title), asc/desc

Update JobCard (partial merge)

PATCH /api/v1/jobcards/{id}

แก้เฉพาะฟิลด์ที่ส่งมา — ฟิลด์ที่ไม่ส่ง ไม่เปลี่ยน. ฟิลด์ read-only/group-C ส่งมาจะได้ 400

{
  "status": "In Progress",
  "priority": "Urgent",
  "customFields": [ { "templateFieldUid": "fld_area", "value": "ทั้งบ้าน" } ]
}

Delete JobCard (soft)

DELETE /api/v1/jobcards/{id}

ลบแบบ soft (ย้ายไป trash กู้คืนได้) — คืน quota, cleanup แม่-ลูก, recalc board summary. ไม่มี hard delete ใน v1

Response: 200 OK

{
  "success": true,
  "data": { "id": "card_XYZ", "action": "soft-deleted" },
  "requestId": "req_xxxxxxxxxxxxx"
}

Lookups (id sources)

เส้น read-only สำหรับหา id ที่ JobCard ต้องใช้ ทั้งหมด workspace-scoped ตาม token

id บน JobCard → เอามาจาก endpoint ไหน

id Endpoint
boardId GET /api/v1/boards
laneId GET /api/v1/boards/{boardId}/lanes
assignedTo / watchers / collaborators / actorUserId GET /api/v1/members
customFields[].templateFieldUid GET /api/v1/boards/{boardId}/custom-fields
customerId GET /api/v1/customers
items[].productId GET /api/v1/products

List Boards

GET /api/v1/boards
{
  "success": true,
  "data": {
    "boards": [
      { "id": "board_abc", "name": "Sales Pipeline", "order": 0, "memberCount": 5 }
    ],
    "pagination": { "page": 1, "limit": 50, "total": 1, "totalPages": 1, "hasMore": false }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

List Lanes of a Board

GET /api/v1/boards/{boardId}/lanes
{
  "success": true,
  "data": {
    "lanes": [
      { "id": "lane_def", "name": "To Do", "order": 0, "boardId": "board_abc", "rules": [] }
    ]
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

Board Custom Fields (schema)

GET /api/v1/boards/{boardId}/custom-fields

คืน schema ของ custom field บน board นั้น ใช้ templateFieldUid เป็นคีย์ตอนส่งค่าใน JobCard

{
  "success": true,
  "data": {
    "boardId": "board_abc",
    "customFields": [
      { "templateFieldUid": "fld_area", "name": "พื้นที่", "type": "text", "options": [], "required": false },
      { "templateFieldUid": "fld_type", "name": "ประเภทงาน", "type": "select", "options": ["ติดตั้ง", "ซ่อม"], "required": true }
    ]
  },
  "requestId": "req_xxxxxxxxxxxxx"
}
รองรับ type: text, number, date, select, radio, multiselect, dropdown (เลื่อน table/signature/upload). ส่ง key ที่ board ไม่มี → 400

List Members

GET /api/v1/members
{
  "success": true,
  "data": {
    "members": [
      { "uid": "uid_seller_7", "displayName": "สมชาย", "email": "somchai@company.com", "photoURL": null }
    ],
    "pagination": { "page": 1, "limit": 100, "total": 1, "totalPages": 1, "hasMore": false }
  },
  "requestId": "req_xxxxxxxxxxxxx"
}

Error Handling

Error Response Format

{
  "error": "error_code",
  "message": "Human readable error message",
  "details": {
    "additional": "error details"
  },
  "requestId": "req_xxxxxxxxxxxxx",
  "timestamp": "2025-11-06T07:00:00.000Z"
}

Common Error Codes

Error Code HTTP Status Description
validation_error 400 ข้อมูลที่ส่งมาไม่ถูกต้อง
invalid_credentials 401 API Key หรือ Secret ไม่ถูกต้อง
invalid_token 401 JWT Token ไม่ถูกต้องหรือหมดอายุ
forbidden 403 ไม่มีสิทธิ์เข้าถึง
bot_detected 403 ตรวจพบพฤติกรรมที่น่าสงสัย
not_found 404 ไม่พบข้อมูลที่ต้องการ
rate_limit_exceeded 429 เกินจำนวน requests ที่กำหนด
internal_error 500 เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์

Security Best Practices

เก็บ API Secret ให้ปลอดภัย

อย่าเก็บ secret ใน code หรือ version control ใช้ environment variables

ใช้ HTTPS เสมอ

อย่าส่ง API requests ผ่าน HTTP

ใช้ Idempotency Keys

ช่วยป้องกันการสร้างข้อมูลซ้ำ

Handle Rate Limits

ตรวจสอบ response headers และ implement exponential backoff

Code Examples

JavaScript Example

// 1. Get JWT Token
async function getAccessToken() {
  const response = await fetch('/api/v1/auth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      apiKey: 'your-api-key',
      apiSecret: 'your-api-secret'
    })
  });
  const data = await response.json();
  return data.data.accessToken;
}

// 2. Create Customer
async function createCustomer(token, customerData) {
  const response = await fetch('/api/v1/customers/upsert', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
      'Idempotency-Key': `create-${Date.now()}`
    },
    body: JSON.stringify(customerData)
  });
  return response.json();
}

// 3. List Customers
async function listCustomers(token, page = 1, limit = 20) {
  const response = await fetch(
    `/api/v1/customers?page=${page}&limit=${limit}`,
    {
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );
  return response.json();
}

Rate Limiting Example

async function callAPIWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);

    // Check rate limit headers
    const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
    console.log(`API calls remaining: ${remaining}`);

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After'));
      console.log(`Rate limited. Retry after ${retryAfter} seconds`);
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      continue;
    }

    return response;
  }

  throw new Error('Max retries exceeded');
}

เชื่อมต่อผ่าน Google Sheets (Google Apps Script)

ลูกค้าที่เก็บออเดอร์ไว้ใน Google Sheet สามารถเชื่อมเข้า Sellstory ได้โดยตรงผ่าน UrlFetchApp ของ Google Apps Script — เรียก endpoint เดียวกับ flow ปกติทุกเส้น (auth → customers/products upsert → jobcards) ไม่ต้องมีเซิร์ฟเวอร์กลาง

สำคัญ! ใช้ base URL จริง https://openapi.sellstory.me/ ตามค่าเริ่มต้นได้เลย Apps Script รันบนเซิร์ฟเวอร์ของ Google ไม่ใช่เครื่องผู้ใช้ — ยิงเข้า http://localhost ไม่ได้ (ใช้ได้เฉพาะตอน test กับ dev server ผ่าน ngrok เท่านั้น) และควรตั้ง header User-Agent เองเสมอ เพื่อไม่ให้โดน bot-detection บล็อคเมื่อ environment: "production"

ขั้นตอน:

  1. เปิด Google Sheet → Extensions → Apps Script
  2. วางสคริปต์ตัวอย่างพร้อมใช้ (มีเมนู UI ในตัว): testscript/google-apps-script/SellstoryOrderSync.gs
  3. CONFIG.BASE_URL ตั้งเป็น https://openapi.sellstory.me/ ไว้ให้แล้ว ไม่ต้องแก้ (แก้เฉพาะถ้าจะ test กับ dev server ผ่าน ngrok) — แก้ CONFIG.WORKSPACE_ID ให้ตรงกับของจริง
  4. บันทึก รีเฟรช Sheet แล้วใช้เมนู "Sellstory API" ที่ปรากฏขึ้น: เชื่อมต่อ → ทดสอบเชื่อมต่อ → สร้าง Header ตัวอย่าง → Sync แถวที่เลือก → JobCard
// Core pattern (ไฟล์เต็มพร้อมเมนู UI + sync ทั้งชีต อยู่ที่
// testscript/google-apps-script/SellstoryOrderSync.gs)

var CONFIG = {
  BASE_URL: 'https://openapi.sellstory.me/',
  WORKSPACE_ID: 'your-workspace-id'
};

function httpJson_(method, path, token, body) {
  var options = {
    method: method,
    muteHttpExceptions: true,
    headers: {
      'User-Agent': 'Mozilla/5.0 (compatible; SellstoryGoogleSheetsSync/1.0)',
      'Accept': 'application/json'
    }
  };
  if (token) options.headers.Authorization = 'Bearer ' + token;
  if (body) {
    options.contentType = 'application/json';
    options.payload = JSON.stringify(body);
  }
  var res = UrlFetchApp.fetch(CONFIG.BASE_URL + path, options);
  return { status: res.getResponseCode(), json: JSON.parse(res.getContentText()) };
}

function syncRowToJobCard(token, boardId, laneId, row) {
  var jobcard = httpJson_('POST', '/api/v1/jobcards', token, {
    boardId: boardId,
    laneId: laneId,
    title: '[GSHEET] ' + row.customerName,
    status: 'Pending',
    items: [{ productId: row.productId, name: row.productName, quantity: row.qty, pricePerUnit: row.price }]
  });
  return jobcard.json.data;
}