Sellstory Open API v1
Complete integration reference for the Customer & Product APIs — authentication, endpoints, request/response schemas, error handling, and code examples.
Authentication
https://openapi.sellstory.me/ — ทุก endpoint path ในเอกสารนี้ (เช่น /api/v1/auth/token) ให้ต่อท้าย base URL นี้เมื่อเรียกใช้งานจริง
1. Request API Credentials
ขอ API Key และ API Secret สำหรับ workspace ของคุณ
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 อื่นๆ
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"
}
Customer APIs
3. Create/Update Customer (Upsert)
สร้างหรืออัปเดตข้อมูลลูกค้า (ระบบจะค้นหาด้วย customId หรือ email ถ้ามีอยู่แล้วจะอัปเดต ถ้าไม่มีจะสร้างใหม่)
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)
- ถ้า 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
5. Get Customer by Custom ID
6. List Customers
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 ถ้ามีอยู่แล้วจะอัปเดต ถ้าไม่มีจะสร้างใหม่)
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"} |
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
ดึงข้อมูลสินค้าด้วย Firestore Document ID
9. Get Product by Custom ID
ดึงข้อมูลสินค้าด้วยรหัสสินค้าจากระบบของคุณ
10. Get Product by SKU
ดึงข้อมูลสินค้าด้วยรหัส SKU (ไม่สนใจตัวพิมพ์เล็ก-ใหญ่)
11. Get Product by Barcode
ดึงข้อมูลสินค้าด้วยบาร์โค้ด
12. List Products
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
id) ไม่ใช่ upsert เพราะ customId ถูก generate โดยระบบ และการ์ดถูกอ้างอิงด้วย id. ทุก id ที่ต้องใช้ (boardId, laneId, assignee, custom field) ดูได้จาก Lookups
Create JobCard
สร้าง Job Card ใหม่บน board + lane ที่ระบุ ระบบจะ generate id + customId ให้
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) |
- |
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"
}
externalId เดิมซ้ำ → คืนการ์ดเดิม (200) ไม่สร้างซ้ำ
Get JobCard by ID
คืน card เต็มทุกฟิลด์
List JobCards
คืนแบบ 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)
แก้เฉพาะฟิลด์ที่ส่งมา — ฟิลด์ที่ไม่ส่ง ไม่เปลี่ยน. ฟิลด์ read-only/group-C ส่งมาจะได้ 400
{
"status": "In Progress",
"priority": "Urgent",
"customFields": [ { "templateFieldUid": "fld_area", "value": "ทั้งบ้าน" } ]
}
Delete JobCard (soft)
ลบแบบ 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
{
"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
{
"success": true,
"data": {
"lanes": [
{ "id": "lane_def", "name": "To Do", "order": 0, "boardId": "board_abc", "rules": [] }
]
},
"requestId": "req_xxxxxxxxxxxxx"
}
Board Custom Fields (schema)
คืน 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"
}
text, number, date, select, radio, multiselect, dropdown (เลื่อน table/signature/upload). ส่ง key ที่ board ไม่มี → 400
List 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) ไม่ต้องมีเซิร์ฟเวอร์กลาง
https://openapi.sellstory.me/ ตามค่าเริ่มต้นได้เลย Apps Script รันบนเซิร์ฟเวอร์ของ Google ไม่ใช่เครื่องผู้ใช้ — ยิงเข้า http://localhost ไม่ได้ (ใช้ได้เฉพาะตอน test กับ dev server ผ่าน ngrok เท่านั้น) และควรตั้ง header User-Agent เองเสมอ เพื่อไม่ให้โดน bot-detection บล็อคเมื่อ environment: "production"
ขั้นตอน:
- เปิด Google Sheet → Extensions → Apps Script
- วางสคริปต์ตัวอย่างพร้อมใช้ (มีเมนู UI ในตัว):
testscript/google-apps-script/SellstoryOrderSync.gs CONFIG.BASE_URLตั้งเป็นhttps://openapi.sellstory.me/ไว้ให้แล้ว ไม่ต้องแก้ (แก้เฉพาะถ้าจะ test กับ dev server ผ่าน ngrok) — แก้CONFIG.WORKSPACE_IDให้ตรงกับของจริง- บันทึก รีเฟรช 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;
}