FitUP Open API FitUP Open API
A machine-to-machine (M2M) JSON API for connecting your system — POS, e-commerce, partner app — to a FitUP tenant. Authenticate once, then call the scoped endpoints below. API แบบ machine-to-machine (M2M) รูปแบบ JSON สำหรับเชื่อมระบบของคุณ — POS, e-commerce หรือแอปพาร์ทเนอร์ — เข้ากับผู้เช่า (tenant) ของ FitUP: ยืนยันตัวตนครั้งเดียว แล้วเรียก endpoint ตามสิทธิ์ที่ได้รับ
Overviewภาพรวม
The Open API is a gateway. It authenticates your client, checks the scope for the operation, resolves your tenant from the token — never from anything you send — and forwards the call to the platform service that owns the data (members, catalog and stock, credit wallet, payments). Requests and responses are JSON throughout. Open API ทำหน้าที่เป็นเกตเวย์: ตรวจสอบตัวตนของ client, ตรวจสิทธิ์ (scope) ของงานนั้น, ระบุ tenant จากตัว token เท่านั้น (ไม่ใช้ค่าที่ส่งมาจากฝั่งคุณ) แล้วส่งต่อไปยังบริการที่เป็นเจ้าของ ข้อมูลจริง (สมาชิก, แคตตาล็อกและสต็อก, เครดิต, การชำระเงิน) — ทุก request/response เป็น JSON
| Itemหัวข้อ | Valueค่า |
|---|---|
| Base URLBase URL |
https://<your-open-api-host>
— sandbox and production hosts are issued with your credentials.
— โฮสต์ sandbox และ production จะให้มาพร้อมกับ credentials ของคุณ
|
| Content-Type |
application/json
on every request that has a body.
สำหรับทุก request ที่มี body
|
| Auth headerHeader ยืนยันตัวตน | Authorization: Bearer <access_token> |
| CredentialsCredentials |
clientId + clientSecret
issued by FitUP. Keep the secret server-side — never ship it in a browser
or mobile app.
ออกให้โดย FitUP — เก็บ secret ไว้ฝั่งเซิร์ฟเวอร์เท่านั้น ห้ามฝังในเว็บเบราว์เซอร์
หรือแอปมือถือ
|
| TenantTenant | Bound to your client at issuance. One client sees exactly one tenant's data. ผูกกับ client ตั้งแต่ตอนออก token — หนึ่ง client เห็นข้อมูลของ tenant เดียวเท่านั้น |
| Specสเปก | openapi.yaml — OpenAPI 3.1, the machine-readable source of truth for every shape below. — OpenAPI 3.1 เป็นต้นฉบับที่เครื่องอ่านได้ของทุกโครงสร้างข้อมูลในหน้านี้ |
Authenticationการยืนยันตัวตน
Exchange your client credentials for a bearer token (client-credentials grant), then
send that token on every subsequent call. Tokens are valid for expires_in seconds
(default 86400 = 24 hours); request a new one when it expires — there is no refresh
token. Cache it, do not mint one per request.
แลก credentials ของคุณเป็น bearer token (client-credentials grant) แล้วแนบ token นั้น
ไปกับทุก request ถัดไป — token มีอายุ expires_in วินาที (ค่าเริ่มต้น 86400
= 24 ชั่วโมง) หมดอายุแล้วขอใหม่ (ไม่มี refresh token) แนะนำให้ cache ไว้ ไม่ต้องขอใหม่ทุก request
Issue an access token for your client. The returned JWT carries your client id, tenant and granted scopes. ออก access token ให้ client ของคุณ — JWT ที่ได้จะบรรจุ client id, tenant และ scope ที่ได้รับไว้ภายใน
Requestคำขอ
curl -X POST "$BASE/api/oauth/token" \
-H 'Content-Type: application/json' \
-d '{"clientId":"YOUR_CLIENT_ID","clientSecret":"YOUR_CLIENT_SECRET"}'
Responseการตอบกลับ · 200
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400
}
401 { "error": "Invalid client credentials" }
— the same message for an unknown client, a wrong secret, or a deactivated client, so the endpoint
cannot be used to probe which clients exist.
หาก credentials ไม่ถูกต้องจะได้ 401 { "error": "Invalid client credentials" }
เสมอ — ข้อความเดียวกันทั้งกรณี client ไม่มีอยู่, secret ผิด หรือ client ถูกปิดใช้งาน เพื่อไม่ให้ใช้ endpoint นี้
สืบว่ามี client ใดอยู่ในระบบ
GET /api/me with it to inspect your token — client id,
tenant, scopes and expiry — without decoding the JWT yourself.
อยากรู้ว่า token ให้สิทธิ์อะไรบ้าง? เรียก
GET /api/me ด้วย token นั้นเพื่อตรวจสอบ token ของคุณ —
client id, tenant, scope และวันหมดอายุ — โดยไม่ต้องถอดรหัส JWT เอง
Tells you what the token you just sent is: the client id it was issued to,
the tenant it is bound to, the scopes it grants, and when it expires. Readable with any valid
token (it only ever describes your own token) — use it to confirm your grants before
investigating a 403 { "error": "Insufficient scopes" }, or to decide when to
request a fresh token.
บอกว่า token ที่คุณเพิ่งส่งมาเป็นของอะไร: client id ที่ออก token นี้, tenant ที่ผูกไว้,
scope ที่ได้รับ และเวลาหมดอายุ · อ่านได้ด้วย token ที่ใช้ได้ทุกตัว (แสดงเฉพาะข้อมูลของ token ตัวคุณเอง)
— ใช้ตรวจสิทธิ์ที่ได้รับก่อนไล่หาสาเหตุของ 403 { "error": "Insufficient scopes" }
หรือใช้ดูว่าควรขอ token ใหม่เมื่อไร
curl "$BASE/api/me" -H "Authorization: Bearer $TOKEN"
Responseการตอบกลับ · 200
{
"clientId": "acme-pos",
"tenantId": "tenant-123",
"scopes": ["read:members", "read:products", "write:purchases"],
"vatRate": 0.07,
"isVatRegistered": true,
"expiresAt": "2026-07-29T10:00:00Z"
}
expiresAt is RFC3339 in UTC. scopes is always an
array — [] if your client has no grants yet. An expired or malformed token gets
401 here just like anywhere else, so this is also a quick "is my token still
good?" check.
expiresAt เป็นรูปแบบ RFC3339 เวลา UTC · scopes
เป็น array เสมอ — เป็น [] หาก client ของคุณยังไม่ได้รับสิทธิ์ใด ๆ · หาก token หมดอายุ
หรือไม่ถูกต้องจะได้ 401 เช่นเดียวกับ endpoint อื่น จึงใช้เช็คเร็ว ๆ ว่า "token ยังใช้ได้อยู่ไหม"
ได้ด้วย
GET /api/products are VAT-inclusive, so an order-summary screen can
compute every total locally — no request per line item. Read vatRate once from this
endpoint, then, in the smallest currency unit (satang) with half-up rounding:
lineTotal = round(unitPrice × qty) − lineDiscount,
total = Σ lineTotal,
vat = round(total × vatRate ÷ (1 + vatRate)).
The purchase response returns the authoritative vat / vatRate for the
bill — if it ever disagrees with what you displayed, the tenant changed their VAT setting after
your token was issued: request a new token to pick it up.
การคำนวณยอดก่อนชำระเงิน ราคาจาก
GET /api/products เป็นราคาที่รวม VAT แล้ว ดังนั้นหน้าสรุปยอดคำนวณเองได้ทั้งหมด
ไม่ต้องยิง API ทุกครั้งที่หยิบสินค้า อ่าน vatRate จาก endpoint นี้ครั้งเดียว แล้วคำนวณใน
หน่วยสตางค์ (ปัดครึ่งขึ้น): lineTotal = round(unitPrice × qty) − lineDiscount,
total = Σ lineTotal,
vat = round(total × vatRate ÷ (1 + vatRate))
ตัวเลขที่ถือเป็นทางการคือ vat / vatRate ที่ได้จากการสั่งซื้อจริง — หากไม่ตรงกับที่แสดงไว้
หมายความว่า tenant เปลี่ยนการตั้งค่า VAT หลังจากออก token ให้ขอ token ใหม่เพื่อรับค่าล่าสุด
Scopesสิทธิ์ (scopes)
Every protected operation requires exactly one scope, shown on each endpoint card
below. Your token only carries the scopes granted to your client; calling an endpoint without its
scope returns 403 { "error": "Insufficient scopes" }.
ทุก endpoint ที่ต้องยืนยันตัวตนจะต้องมี scope เดียวที่กำหนดไว้ (แสดงบนการ์ดของแต่ละ
endpoint) — token ของคุณมีเพียง scope ที่ client ได้รับ หากเรียกโดยไม่มี scope นั้นจะได้
403 { "error": "Insufficient scopes" }
| Scope | Grantsให้สิทธิ์ |
|---|---|
read:members | GET /api/members |
read:products | GET /api/products, GET /api/product-categories |
write:purchases | POST /api/purchases |
read:stock | GET /api/stock, GET /api/products/{id}/stock, GET /api/products/{id}/stock/summary, GET /api/stock/lots, GET /api/stock/expiring |
write:stock | POST /api/stock/sale, POST /api/stock/sale/{orderNo}/reverse |
read:lead | GET /api/interest-service |
write:lead | POST /api/leads |
write:credit | POST /api/pay-with-credit, POST /api/pay-with-qr |
write:payments | POST /api/payments, POST /api/paylink, POST /api/recurring/schedules, POST /api/recurring/schedules/{scheduleId}/cancel |
read:payments | Status / lookup reads for payments, PayLink and recurring schedules. การอ่านสถานะ/ข้อมูลของ payments, PayLink และ recurring schedules |
| — |
GET /api/me and GET /api/branches need no extra
scope: any valid token may read its own token details and its own tenant's branches. Call
GET /api/me to see the scopes your token actually carries.
GET /api/me และ GET /api/branches ไม่ต้องใช้ scope เพิ่ม
— token ที่ใช้ได้ทุกตัวอ่านข้อมูลของ token ตัวเองและสาขาของ tenant ตัวเองได้ · เรียก
GET /api/me เพื่อดูว่า token ของคุณมี scope อะไรบ้าง
|
Conventionsข้อกำหนดร่วม
The branch-id headerHeader branch-id
Several endpoints take a branch-id request header carrying a branch id
from GET /api/branches. It is optional on members and products (omit it
to span every branch), required on purchases, and defaults to 1 on
leads. On products it does double duty: it filters the catalog and attaches that branch's
on-hand stock to each item.
หลาย endpoint รับ header branch-id ซึ่งใส่รหัสสาขาที่ได้จาก
GET /api/branches — ไม่บังคับ สำหรับสมาชิกและสินค้า (ไม่ใส่ = ทุกสาขา),
บังคับ สำหรับการขาย (purchases) และมีค่าเริ่มต้นเป็น 1 สำหรับ leads
ส่วนกรณีสินค้า header นี้ทำสองหน้าที่: กรองแคตตาล็อก และ แนบ stock
คงเหลือของสาขานั้นมาให้ในแต่ละรายการ
Paginationการแบ่งหน้า
List endpoints accept page (default 1) and pageSize
(default 10) and answer with a pagination envelope:
Endpoint ที่เป็นรายการรับ page (ค่าเริ่มต้น 1) และ pageSize
(ค่าเริ่มต้น 10) และตอบกลับพร้อมส่วนหัว pagination:
{ "pagination": { "total": 100, "page": 1, "pageSize": 10 }, "data": [ ... ] }
IdempotencyIdempotency (เรียกซ้ำได้ปลอดภัย)
Every write that moves money or stock is idempotent by a key you supply, so a retry after a timeout never double-charges or double-deducts. Always reuse the same key when retrying the same logical operation. ทุก endpoint ที่เขียนข้อมูลเกี่ยวกับเงินหรือสต็อกเป็น idempotent ด้วยคีย์ที่ คุณ กำหนดเอง ดังนั้นการ retry หลัง timeout จะไม่ตัดเงินหรือตัดสต็อกซ้ำ — ให้ใช้คีย์เดิมทุกครั้งที่ retry งานเดียวกัน
| EndpointsEndpoint | Keyคีย์ | On retryเมื่อ retry |
|---|---|---|
| Payments · PayLink · RecurringPayments · PayLink · Recurring | orderNo |
Replays the original response with header Idempotent-Replay: true.
A 409 means the first attempt is still in flight — retry shortly.
ส่งผลลัพธ์เดิมกลับมาพร้อม header Idempotent-Replay: true —
หากได้ 409 หมายความว่าคำขอแรกยังทำงานอยู่ ให้ลองใหม่อีกครั้งในอีกไม่นาน
|
| Stock sale / reverseตัดสต็อก / คืนสต็อก | orderNo |
Returns "applied": false (or "reversed": false)
without touching stock again.
ตอบ "applied": false (หรือ "reversed": false)
โดยไม่แตะสต็อกอีก
|
| Pay with credit / QRตัดเครดิต / QR | receiptNo |
Returns success without deducting again (no 409).
ตอบสำเร็จโดยไม่ตัดเครดิตซ้ำ (ไม่มี 409)
|
Tracing (X-Correlation-Id)การติดตามคำขอ (X-Correlation-Id)
Send an optional X-Correlation-Id header (any string; X-Request-Id
is honored too) on any request. It is echoed on the response and propagated to every internal
service, so one id stitches the whole chain in our logs — quote it to support and we find your exact
request. Omit it and the gateway generates one for you and returns it.
คุณสามารถแนบ header X-Correlation-Id (ข้อความอะไรก็ได้ และรองรับ
X-Request-Id ด้วย) มากับ request ใดก็ได้ — ค่านี้จะถูกส่งกลับมาใน response
และส่งต่อไปยังทุกบริการภายใน ทำให้ id เดียวเชื่อมทั้งสายใน log ของเรา แจ้ง id นี้กับทีมซัพพอร์ต
แล้วเราจะหา request ของคุณได้ตรงตัว หากไม่ส่งมา เกตเวย์จะสร้างให้และส่งกลับใน response
Unitsหน่วย
| Fieldฟิลด์ | Unitหน่วย |
|---|---|
price · amount · total |
Baht, major units — 89.90 means ฿89.90 (never satang).
บาท (หน่วยใหญ่) — 89.90 คือ ฿89.90 ไม่ใช่สตางค์
|
credit | Credit units (number), the member's wallet balance unit. หน่วยเครดิต (ตัวเลข) ตามยอดกระเป๋าเงินของสมาชิก |
qty · quantity |
Units, decimals allowed up to 3 places — 2.5.
จำนวนหน่วย รองรับทศนิยมไม่เกิน 3 ตำแหน่ง เช่น 2.5
|
createdAt · updatedAt | ISO-8601 timestamps (UTC). เวลาแบบ ISO-8601 (UTC) |
Errorsข้อผิดพลาด
Gateway errors are always { "error": "<message>" }. When an
internal service rejects the call, its 4xx status and body are passed through
unchanged. Internal 5xx, timeouts and transport failures are collapsed into
502 — that is our problem, not your payload's, and the request is safe to retry with
the same idempotency key.
ข้อผิดพลาดของเกตเวย์อยู่ในรูป { "error": "<message>" } เสมอ
หากบริการภายในปฏิเสธคำขอ สถานะ 4xx และ body ของบริการนั้นจะถูกส่งผ่านกลับมาตรง ๆ
ส่วน 5xx, timeout และปัญหาการเชื่อมต่อภายในจะถูกแปลงเป็น 502
ทั้งหมด — กรณีนี้เป็นปัญหาฝั่งเรา ไม่ใช่ข้อมูลที่คุณส่ง และปลอดภัยที่จะ retry ด้วยคีย์ idempotency เดิม
| Status | Meaningความหมาย | Typical bodyตัวอย่าง body |
|---|---|---|
| 400 | Malformed body or an invalid field.body ผิดรูปแบบ หรือฟิลด์ไม่ถูกต้อง | {"error":"invalid request body"} |
| 401 | Missing / expired token, or bad client credentials.ไม่มี token, token หมดอายุ หรือ credentials ไม่ถูกต้อง | {"error":"Invalid or expired token"} |
| 403 | Token lacks the scope, or its tenant is no longer active.token ไม่มี scope ที่ต้องใช้ หรือ tenant ไม่ได้ใช้งานแล้ว | {"error":"Insufficient scopes"} |
| 404 | Resource not found (passed through from the owning service).ไม่พบข้อมูล (ส่งผ่านมาจากบริการเจ้าของข้อมูล) | {"error":"not found"} |
| 409 | Insufficient stock, or an idempotent request still in flight.สต็อกไม่พอ หรือคำขอ idempotent เดิมยังทำงานอยู่ | {"error":"a request with this orderNo is already in progress"} |
| 502 | An internal service errored, timed out or was unreachable.บริการภายในผิดพลาด, timeout หรือติดต่อไม่ได้ | {"error":"downstream unavailable"} |
Membersสมาชิก
List the tenant's members, newest first, paginated. query is a
free-text search over name, nickname, phone and member code.
แสดงรายการสมาชิกของ tenant แบบแบ่งหน้า — query เป็นการค้นหาข้อความอิสระ
จากชื่อ, ชื่อเล่น, เบอร์โทร และรหัสสมาชิก
| Paramพารามิเตอร์ | Inที่ | Notesหมายเหตุ |
|---|---|---|
page | query | Default 1.ค่าเริ่มต้น 1 |
pageSize | query | Default 10.ค่าเริ่มต้น 10 |
query | query | Free text; trimmed and lower-cased server-side.ข้อความอิสระ ระบบตัดช่องว่างและแปลงเป็นตัวพิมพ์เล็กให้ |
branch-id | header | Optional — only members of that branch. Omit for every branch.ไม่บังคับ — เฉพาะสมาชิกของสาขานั้น ไม่ใส่ = ทุกสาขา |
curl "$BASE/api/members?page=1&pageSize=10&query=somchai" \
-H "Authorization: Bearer $TOKEN" \
-H 'branch-id: 2'
{
"pagination": { "total": 100, "page": 1, "pageSize": 10 },
"data": [
{
"id": 123,
"firstName": "Somchai",
"lastName": "Jaidee",
"nickName": "Chai",
"phone": "+66812345678",
"dateOfBirth": "1990-05-21T00:00:00.000Z",
"memberCode": "M0001",
"creditBalance": 500,
"imageUrl": "https://cdn.example.com/avatars/123.jpg"
}
]
}
imageUrl and dateOfBirth may be null.
creditBalance is the member's current wallet balance. The old snake_case names
(first_name, member_code, credit_balance, …) are gone —
every field on this response is camelCase.
imageUrl และ dateOfBirth อาจเป็น null ได้
creditBalance คือยอดกระเป๋าเงินคงเหลือปัจจุบัน — ชื่อฟิลด์แบบ snake_case เดิม
(first_name, member_code, credit_balance ฯลฯ) ถูกยกเลิกแล้ว
ทุกฟิลด์ใน response นี้เป็น camelCase
Productsสินค้า
List the tenant's catalog, paginated. Send the branch-id header to
scope to one branch (that branch's products plus the ones sold at every branch) and to receive
each item's on-hand stock inline.
แสดงแคตตาล็อกสินค้าของ tenant แบบแบ่งหน้า — ใส่ header branch-id
เพื่อจำกัดเฉพาะสาขานั้น (สินค้าของสาขานั้น บวกสินค้าที่ขายทุกสาขา) และจะได้สต็อกคงเหลือของแต่ละรายการ
แนบมาด้วย
| Paramพารามิเตอร์ | Inที่ | Notesหมายเหตุ |
|---|---|---|
page · pageSize | query | Default 1 / 10.ค่าเริ่มต้น 1 / 10 |
query | query | Free-text search over title and SKU.ค้นหาข้อความอิสระจากชื่อสินค้าและ SKU |
paymentType | query |
CASH or CREDIT. Prefer this camelCase name;
payment_type is a deprecated alias kept for old integrations.
CASH หรือ CREDIT — ให้ใช้ชื่อแบบ camelCase นี้
ส่วน payment_type เป็นชื่อเดิมที่เลิกใช้แล้ว แต่ยังรับไว้เพื่อความเข้ากันได้
|
categoryId | query | Only products in this category (see below).เฉพาะสินค้าในหมวดนี้ (ดูหัวข้อถัดไป) |
branch-id | header | Optional; also attaches stock per item.ไม่บังคับ และจะแนบ stock ของแต่ละรายการมาให้ |
curl "$BASE/api/products?page=1&pageSize=10&paymentType=CASH&categoryId=3" \
-H "Authorization: Bearer $TOKEN" \
-H 'branch-id: 2'
{
"pagination": { "total": 42, "page": 1, "pageSize": 10 },
"data": [
{
"id": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81",
"title": "Protein Shake",
"description": "Chocolate",
"skuCode": "PS-001",
"paymentType": "CASH",
"price": 89.90,
"unit": "bottle",
"isActive": true,
"trackStock": true,
"branchIds": [],
"branchId": null,
"imageUrl": "https://cdn.example.com/products/ps-001.jpg",
"categoryId": 3,
"createdAt": "2026-01-10T08:00:00.000Z",
"updatedAt": "2026-02-01T09:30:00.000Z",
"stock": { "branchId": 2, "quantity": 24, "reorderLevel": 6, "lowStock": false }
}
]
}
branchIds lists the branches that sell the product; an empty
array means every branch. (branchId is the deprecated single-branch view: it is
set only when the product sits on exactly one branch.)
trackStock: true means stock is deducted when it is sold; false is
catalog-only. The stock object appears only when you send the
branch-id header.
branchIds คือรายการสาขาที่ขายสินค้านั้น ถ้าเป็น array ว่าง
หมายถึงขายทุกสาขา (branchId เป็นฟิลด์เดิมที่เลิกใช้แล้ว จะมีค่าเฉพาะกรณีที่สินค้า
ผูกกับสาขาเดียวเท่านั้น) · trackStock: true หมายถึงระบบจะตัดสต็อกเมื่อขาย ส่วน
false คือเก็บไว้ในแคตตาล็อกเท่านั้น · ออบเจ็กต์ stock
จะมีมาเฉพาะเมื่อคุณส่ง header branch-id
The tenant's product categories (flat, one level). Pass an id as
?categoryId= on GET /api/products. Query active=true for
active categories only.
หมวดสินค้าของ tenant (ชั้นเดียว ไม่มีหมวดย่อย) — นำ id ไปใส่เป็น
?categoryId= ใน GET /api/products · ใส่ active=true
เพื่อเอาเฉพาะหมวดที่เปิดใช้งาน
curl "$BASE/api/product-categories?active=true" \
-H "Authorization: Bearer $TOKEN"
[
{
"id": 3,
"nameTh": "อาหารเสริม",
"nameEn": "Supplements",
"name": "อาหารเสริม",
"sortOrder": 0,
"isActive": true,
"isDefault": false
},
{
"id": 7,
"nameTh": "อื่นๆ",
"nameEn": "Other",
"name": "อื่นๆ",
"sortOrder": 9999,
"isActive": true,
"isDefault": true
}
]
nameTh and
nameEn, at least one of which is non-empty — pick the language your UI needs.
name is a read-only legacy field (nameTh when set, otherwise
nameEn). Every tenant always has one isDefault: true category,
อื่นๆ / Other: it sorts last, cannot be deleted, and holds products created
without an explicit category.
ชื่อหมวดเป็นสองภาษา: nameTh และ nameEn
โดยมีอย่างน้อยหนึ่งค่าที่ไม่ว่าง — เลือกใช้ภาษาที่ UI ของคุณต้องการ · name เป็นฟิลด์เดิม
แบบอ่านอย่างเดียว (เท่ากับ nameTh ถ้ามี ไม่มีก็ใช้ nameEn) · ทุก tenant
จะมีหมวด isDefault: true ชื่อ อื่นๆ / Other อยู่หนึ่งหมวดเสมอ
เรียงท้ายสุด ลบไม่ได้ และใช้เก็บสินค้าที่สร้างโดยไม่ระบุหมวด
Branchesสาขา
The tenant's active branches — reference data, readable with any valid token
(you only ever see your own tenant's branches). Use an id from here as the
branch-id header elsewhere.
สาขาที่เปิดใช้งานของ tenant — เป็นข้อมูลอ้างอิงที่อ่านได้ด้วย token ที่ใช้ได้ทุกตัว
(คุณเห็นเฉพาะสาขาของ tenant ตัวเอง) · นำ id จากที่นี่ไปใส่เป็น header
branch-id ในการเรียกอื่น ๆ
curl "$BASE/api/branches" -H "Authorization: Bearer $TOKEN"
[
{ "id": 1, "code": "HQ", "name": "Headquarters" },
{ "id": 2, "code": "BR-02", "name": "Sukhumvit" }
]
Check that a POS machine serial is registered to a branch before you
start sending it as posId on sales. Send the serial printed on the machine with the
branch-id header of the branch it stands in (required) and you get the
machine back; send one that is not registered to that branch and you get
400 POS_NOT_FOUND. Readable with any valid token — it only confirms a machine that is
already yours, so no extra scope.
ใช้ตรวจว่าหมายเลขเครื่อง (serial) ของเครื่อง POS ลงทะเบียนไว้กับสาขานั้นจริง
ก่อนเริ่มส่งเป็น posId ในการขาย · ส่ง serial ที่พิมพ์อยู่บนตัวเครื่อง
พร้อม header branch-id ของสาขาที่เครื่องตั้งอยู่ (บังคับ) ระบบจะคืนข้อมูล
เครื่องกลับมา · หากส่งค่าที่ไม่ได้ลงทะเบียนกับสาขานั้น จะได้ 400 POS_NOT_FOUND ·
เรียกได้ด้วย token ที่ใช้ได้ทุกตัว เพราะเป็นเพียงการยืนยันเครื่องที่เป็นของคุณอยู่แล้ว
จึงไม่ต้องมี scope เพิ่ม
curl -X POST "$BASE/api/branch-pos/activate" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'branch-id: 2' \
-d '{ "posId": "E030230003A0050" }'
{
"id": "7c1f0a94-3b52-4de6-9f18-6c0b2d8ae431",
"posId": "E030230003A0050",
"posNumber": "NP01",
"branchId": 2
}
posNumber is the machine number shown on the receipt, and the prefix of
that machine's tax-invoice numbers. Machines are registered by the FitUP platform admin — contact the
platform to have yours added; there is no self-service registration.
posNumber คือหมายเลขเครื่องที่แสดงบนใบเสร็จ และเป็นคำนำหน้าของเลข
ใบกำกับภาษีที่ออกจากเครื่องนั้น · การลงทะเบียนเครื่องทำโดยผู้ดูแลระบบของ FitUP —
กรุณาติดต่อทีมงานเพื่อลงทะเบียนเครื่องของคุณ ไม่สามารถลงทะเบียนเองได้
Purchasesการขาย (Purchases)
Record a complete sale in one call: validates the products, deducts stock for
trackStock products, deducts the member's credit wallet on CREDIT
payments, writes the receipt and returns the totals. The branch-id header is
required.
บันทึกการขายให้ครบในคำขอเดียว: ตรวจสอบสินค้า, ตัดสต็อกสำหรับสินค้าที่
trackStock, ตัดเครดิตของสมาชิกเมื่อชำระแบบ CREDIT, บันทึกใบเสร็จ
และคืนยอดรวมกลับมา · header branch-id บังคับ
| Fieldฟิลด์ | Requiredบังคับ | Notesหมายเหตุ |
|---|---|---|
items[].productId | ✓ | Product id from the catalog.รหัสสินค้าจากแคตตาล็อก |
items[].qty | ✓ | Up to 3 decimals.ทศนิยมไม่เกิน 3 ตำแหน่ง |
items[].lineDiscount | — | Per-line discount in baht.ส่วนลดรายบรรทัด (บาท) |
cashierId | — | Staff user id credited as the salesperson. Omit it and the sale is credited to the fitness's default cashier; send it to override that for one sale.รหัสผู้ใช้พนักงานที่บันทึกเป็นผู้ขาย — หากไม่ส่ง ระบบจะบันทึกเป็นพนักงานเริ่มต้น (default cashier) ของฟิตเนสนั้น · ส่งมาเมื่อต้องการระบุพนักงานเฉพาะรายการนั้น |
posId | — | Serial of the POS machine ringing up the sale, as printed on the machine
(e.g. E030230003A0050). Stamps that machine on the receipt and switches the
tax-invoice number to the machine's own series. Omit it and the receipt carries no machine number
(the branch's own series is used instead). A posId that is not a machine registered to
this branch is rejected with 400 POS_NOT_FOUND. Machines are registered by the FitUP
platform admin — contact the platform to have yours added; there is no self-service
registration.หมายเลขเครื่อง (serial) ของเครื่อง POS ที่ใช้ขาย ตามที่พิมพ์ไว้บนตัวเครื่อง
(เช่น E030230003A0050) — ระบบจะบันทึกเครื่องนี้ไว้บนใบเสร็จ และออกเลขใบกำกับภาษีจากชุดเลข
ของเครื่องนั้น · หากไม่ส่ง ใบเสร็จจะไม่มีหมายเลขเครื่อง (ใช้ชุดเลขของสาขาแทน) · หากส่ง posId
ที่ไม่ได้ลงทะเบียนไว้กับสาขานี้ จะได้ 400 POS_NOT_FOUND · การลงทะเบียนเครื่องทำโดยผู้ดูแล
ระบบของ FitUP (commander) — กรุณาติดต่อทีมงานเพื่อลงทะเบียนเครื่องของคุณ ไม่สามารถลงทะเบียนเองได้ |
paymentType | — | CASH (default) or CREDIT.CASH (ค่าเริ่มต้น) หรือ CREDIT |
memberId | — | Required when paymentType = CREDIT.บังคับเมื่อ paymentType = CREDIT |
receiptPaymentMethod | — | Tender recorded on the receipt for CASH sales (default CASH); ignored for CREDIT.วิธีรับชำระที่บันทึกบนใบเสร็จสำหรับการขายแบบ CASH (ค่าเริ่มต้น CASH) — ไม่ใช้กับ CREDIT |
paid | — | Amount tendered in baht; defaults to the total.จำนวนเงินที่รับมา (บาท) — ค่าเริ่มต้นเท่ากับยอดรวม |
curl -X POST "$BASE/api/purchases" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'branch-id: 2' \
-d '{
"items": [
{ "productId": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81", "qty": 2 },
{ "productId": "b1c2d3e4-7a88-4d0a-9c31-55e0a7f2c410", "qty": 1, "lineDiscount": 25.00 }
],
"cashierId": 456,
"paymentType": "CASH",
"receiptPaymentMethod": "CASH",
"paid": 250.00
}'
cashierId from that body and the sale is credited to the fitness's
default cashier — you never need to know FitUP's internal staff ids. The fitness sets that staff member
in FitUP's staff management, and product scopes are not granted until it is set, so in practice you can
always omit cashierId. The default is per fitness, not per client.
หากตัด cashierId ออกจาก body ระบบจะบันทึกการขายให้พนักงานเริ่มต้น
(default cashier) ของฟิตเนสนั้น — คุณไม่จำเป็นต้องรู้รหัสพนักงานภายในของ FitUP · ฟิตเนสจะตั้งค่าพนักงานคนนี้
ในหน้าจัดการพนักงานของ FitUP และจะออก scope กลุ่มสินค้าให้ไม่ได้ถ้ายังไม่ได้ตั้ง ดังนั้นในทางปฏิบัติคุณ
ไม่ต้องส่ง cashierId เลย · ค่าเริ่มต้นนี้เป็นระดับฟิตเนส ไม่ใช่ระดับ client
{
"success": true,
"purchaseId": "3d9f8a5c-1e2b-47d6-8c90-af7b21d4e6c3",
"receiptId": 1024,
"receiptCode": "RCPT-2026-001024",
"currency": "THB",
"subtotal": 250.00,
"discount": 25.00,
"vat": 14.72,
"vatRate": 0.07,
"total": 225.00,
"paid": 250.00,
"change": 25.00,
"receipt": {
"fitness": { "name": "Fitup Fitness Co., Ltd.", "taxId": "0105561234567", "logoUrl": "https://.../logo.png" },
"branch": {
"name": "สาขาสยาม", "code": "BKK-01", "addressLine": "999 ถนนพระราม 1", "zipCode": "10330",
"officePhone": "021234567", "subDistrict": "ปทุมวัน", "district": "ปทุมวัน", "province": "กรุงเทพมหานคร",
"isHeadOffice": false, "label": "สาขา สาขาสยาม (BKK-01)"
},
"receipt": {
"code": "RCPT-2026-001024", "issuedAt": "2026-08-04 10:30", "vatPercent": 7,
"beforeVat": "210.28", "vat": "14.72", "total": "225.00",
"totalInThai": "สองร้อยยี่สิบห้าบาทถ้วน"
},
"items": [
{ "no": 1, "name": "Protein Shake", "qty": "2.00", "unitPrice": "89.90", "lineDiscount": "0.00", "lineTotal": "179.80" },
{ "no": 2, "name": "Shaker Bottle", "qty": "1.00", "unitPrice": "70.20", "lineDiscount": "25.00", "lineTotal": "45.20" }
],
"member": { "firstName": "สมชาย", "lastName": "ใจดี" },
"cashier": { "firstName": "Staff", "lastName": "One" }
}
}
receipt object is everything printed on a FitUP receipt, so your POS can
lay out the same slip we do — club and branch header, line items, VAT breakdown and the Thai baht text.
Its money and quantity fields are pre-formatted strings ("1,920.00") on
purpose: print them verbatim and your slip matches ours to the satang. Use the numeric fields above
(total, vat, …) for your own bookkeeping. branch / member
can be null (a walk-in sale has no member), and on the rare occasion the receipt cannot be read
back the whole receipt key is absent — the sale still succeeded, so treat it as optional.
For the branch line print branch.label as given (สำนักงานใหญ่ for the head office,
else สาขา {name} ({code})) rather than building it from name / code.
ออบเจ็กต์ receipt คือข้อมูลทุกอย่างที่พิมพ์อยู่บนใบเสร็จของ FitUP เพื่อให้ POS
ของคุณจัดสลิปได้เหมือนกับที่เราออก — หัวใบเสร็จของสาขาและฟิตเนส, รายการสินค้า, การแยก VAT และตัวหนังสือ
จำนวนเงินภาษาไทย · ฟิลด์จำนวนเงิน/จำนวนสินค้าเป็นสตริงที่จัดรูปแบบมาแล้ว
("1,920.00") โดยเจตนา — พิมพ์ตามนี้ตรงๆ สลิปจะตรงกับของเราถึงระดับสตางค์ · ถ้าจะเอาไปคำนวณ
บัญชีเองให้ใช้ตัวเลขด้านบน (total, vat, …) · branch /
member เป็น null ได้ (ขายหน้าร้านไม่มีสมาชิก) และในกรณีที่อ่านใบเสร็จกลับมาไม่ได้
คีย์ receipt จะไม่มีมาเลย แต่การขายสำเร็จแล้ว จึงควรมองว่าเป็นฟิลด์ที่อาจไม่มี ·
บรรทัดสาขาให้พิมพ์ branch.label ตามที่ได้รับ (สำนักงานใหญ่ สำหรับสำนักงานใหญ่
นอกนั้นเป็น สาขา {name} ({code})) ไม่ต้องประกอบเองจาก name / code
POST /api/stock/sale or
POST /api/pay-with-credit for the same sale — this endpoint already deducts both stock
and credit, so pairing them double-deducts. Voiding a receipt is a staff-side operation in the
FitUP back office; it is not exposed here.
อย่าเรียก POST /api/stock/sale หรือ
POST /api/pay-with-credit ซ้ำสำหรับการขายเดียวกัน — endpoint นี้ตัดทั้งสต็อกและเครดิต
ให้แล้ว หากเรียกซ้ำจะถูกตัดสองครั้ง · การยกเลิก (void) ใบเสร็จเป็นงานฝั่งพนักงานในระบบหลังบ้านของ
FitUP ไม่เปิดให้เรียกผ่าน API นี้
Stockสต็อก
On-hand quantities, lot visibility and the stock-first sale flow. Stock is consumed FEFO → FIFO: soonest expiry first, then oldest received. Quantities are decimals. ยอดคงเหลือ, การมองเห็นล็อต และการตัดสต็อกแบบ stock-first · การตัดสต็อกใช้ลำดับ FEFO → FIFO คือหมดอายุก่อนตัดก่อน แล้วจึงเรียงตามล็อตที่รับเข้ามาก่อน · จำนวนเป็นทศนิยมได้
On-hand stock across products, paginated. Query: branchId,
lowStock (true = at or below the reorder level), page,
pageSize.
ยอดคงเหลือของสินค้าทั้งหมดแบบแบ่งหน้า · พารามิเตอร์: branchId,
lowStock (true = ถึงหรือต่ำกว่าจุดสั่งซื้อ), page,
pageSize
curl "$BASE/api/stock?branchId=2&lowStock=true&page=1&pageSize=20" \
-H "Authorization: Bearer $TOKEN"
{
"pagination": { "total": 12, "page": 1, "pageSize": 20 },
"data": [
{
"productId": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81",
"branchId": 2,
"quantity": 4,
"reorderLevel": 6,
"lowStock": true,
"updatedAt": "2026-07-20T04:10:00.000Z"
}
]
}
On-hand for one product across every branch. {id} is the product
id. Returns an array of the same stock rows as above.
ยอดคงเหลือของสินค้าหนึ่งรายการในทุกสาขา — {id} คือรหัสสินค้า
ผลลัพธ์เป็น array ของแถวสต็อกรูปแบบเดียวกับด้านบน
curl "$BASE/api/products/8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81/stock" \
-H "Authorization: Bearer $TOKEN"
What one product holds at one branch and what it is worth:
stockValue is the batches still on hand valued at what each actually cost (FIFO),
not quantity × today's price. branchId is required — stock, and so its value,
only means anything per branch.
ยอดคงเหลือและมูลค่าของสินค้าหนึ่งรายการที่สาขาหนึ่ง — stockValue
คิดจากล็อตที่เหลืออยู่ตามต้นทุนจริงของแต่ละล็อต (FIFO) ไม่ใช่จำนวน × ราคาขายวันนี้ ·
ต้องระบุ branchId เพราะสต็อกและมูลค่ามีความหมายแยกตามสาขาเท่านั้น
curl "$BASE/api/products/8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81/stock/summary?branchId=2" \
-H "Authorization: Bearer $TOKEN"
{
"productId": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81",
"branchId": 2,
"quantity": 15,
"quantityCosted": 15,
"stockValue": 2775,
"avgCost": 185,
"avgSalePrice": 249
}
avgCost and avgSalePrice are null, never
0, when there is nothing to average. quantityCosted below
quantity means some stock was received before a cost was recorded, so
stockValue covers only part of it.
avgCost และ avgSalePrice จะเป็น null
ไม่ใช่ 0 เมื่อไม่มีข้อมูลให้เฉลี่ย · หาก quantityCosted น้อยกว่า
quantity แปลว่ามีสต็อกบางส่วนที่รับเข้ามาโดยไม่ได้บันทึกต้นทุน
stockValue จึงครอบคลุมเพียงบางส่วน
Open lots in consume order (FEFO → FIFO), paginated. Query:
productId, branchId, page, pageSize.
ล็อตที่ยังมีของเหลือ เรียงตามลำดับที่จะถูกตัด (FEFO → FIFO) แบบแบ่งหน้า ·
พารามิเตอร์: productId, branchId, page,
pageSize
curl "$BASE/api/stock/lots?productId=8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81&branchId=2" \
-H "Authorization: Bearer $TOKEN"
{
"pagination": { "total": 3, "page": 1, "pageSize": 10 },
"data": [
{
"id": "0f21a7c4-52d8-4a1e-b6d3-9c8f10a4e772",
"productId": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81",
"branchId": 2,
"lotNo": "LOT-2026-07",
"qtyReceived": 24,
"qtyRemaining": 4,
"receivedAt": "2026-07-01T02:00:00.000Z",
"expiryDate": "2026-09-30",
"refNo": "PO-8891",
"note": ""
}
]
}
Open lots expiring within N days, soonest first — already-expired lots are
included with a negative daysUntilExpiry. Query: days (default 30),
branchId.
ล็อตที่จะหมดอายุภายใน N วัน เรียงจากใกล้หมดอายุที่สุด — รวมล็อตที่หมดอายุแล้ว
โดย daysUntilExpiry จะเป็นค่าติดลบ · พารามิเตอร์: days (ค่าเริ่มต้น 30),
branchId
curl "$BASE/api/stock/expiring?days=30&branchId=2" \
-H "Authorization: Bearer $TOKEN"
[
{
"id": "0f21a7c4-52d8-4a1e-b6d3-9c8f10a4e772",
"productId": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81",
"branchId": 2,
"lotNo": "LOT-2026-07",
"qtyRemaining": 4,
"expiryDate": "2026-08-05",
"daysUntilExpiry": 8,
"expired": false
}
]
Deduct stock for an order's line items — atomic across items and idempotent by
orderNo. If any tracked item lacks stock the whole call fails with 409 and
nothing is deducted; the message names the product
(insufficient stock for product <productId>). A replay of an applied
orderNo returns "applied": false.
ตัดสต็อกตามรายการของออร์เดอร์ — ทำเป็นหน่วยเดียว (atomic) ทุกบรรทัด และเป็น
idempotent ด้วย orderNo · หากมีรายการที่นับสต็อกใดสต็อกไม่พอ ทั้งคำขอจะล้มเหลวด้วย
409 และไม่มีการตัดสต็อกเลย โดยข้อความจะระบุสินค้าที่ไม่พอ
(insufficient stock for product <productId>) · การเรียกซ้ำด้วย
orderNo ที่ตัดไปแล้วจะได้ "applied": false
Only meaningful for products with "trackStock": true. A
catalog-only product ("trackStock": false) has no on-hand by design and is always
sellable, so its line moves no stock and is listed in untracked on the response
instead of failing the order — you never need to know which products the tenant tracks before
selling them.
ใช้ได้จริงกับสินค้าที่ "trackStock": true เท่านั้น · สินค้าที่ไม่นับสต็อก
("trackStock": false) ไม่มียอดคงเหลือโดยการออกแบบและขายได้เสมอ ดังนั้นบรรทัดนั้น
จะไม่ตัดสต็อกและจะถูกแจ้งกลับใน untracked แทนที่จะทำให้ออร์เดอร์ล้มเหลว —
คุณไม่จำเป็นต้องรู้ล่วงหน้าว่าสินค้าใดของร้านนับสต็อกอยู่
curl -X POST "$BASE/api/stock/sale" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'X-Correlation-Id: my-trace-0001' \
-d '{
"orderNo": "SO-1001",
"branchId": 2,
"contact": "Somchai P.",
"actor": "Cashier 01",
"items": [ { "productId": "8f3a1c2e-6b4d-4c19-9f77-2a1e5c0d3b81", "qty": 2, "unitPrice": 199 } ]
}'
{
"success": true,
"applied": true,
"orderNo": "SO-1001",
"untracked": ["4c8b0d71-2f19-4a53-8e6c-9b7d1a2f3e40"]
}
contact, actor and per-line unitPrice
are optional. They are snapshotted onto the stock ledger so the tenant's stock report can show
who sold what and at what price; unitPrice is the gross Baht figure your receipt
prints (no VAT handling), and a line without one is simply left out of the average-sale-price
figure.
contact, actor และ unitPrice ของแต่ละบรรทัด
เป็นค่าเสริม · ระบบจะบันทึกลงในบัญชีเคลื่อนไหวสต็อกเพื่อให้รายงานสต็อกแสดงได้ว่าใครขายอะไรในราคาเท่าใด ·
unitPrice คือราคาต่อหน่วยแบบรวม (ตามที่พิมพ์บนใบเสร็จของคุณ ไม่มีการคำนวณ VAT ซ้ำ)
และบรรทัดที่ไม่ส่งมาจะไม่ถูกนำไปคิดราคาขายเฉลี่ย
untracked appears only when the order contained catalog-only
products; those lines moved no stock.
untracked จะมีเฉพาะเมื่อออร์เดอร์มีสินค้าที่ไม่นับสต็อก —
บรรทัดเหล่านั้นไม่ถูกตัดสต็อก
POST /api/purchases already deducts stock. Use
the stock sale endpoints only when you keep your own billing/receipts and just need FitUP's stock
ledger moved.
POST /api/purchases ตัดสต็อกให้แล้ว —
ใช้ endpoint ตัดสต็อกนี้เฉพาะกรณีที่คุณออกบิล/ใบเสร็จเองในระบบของคุณ และต้องการเพียงให้สต็อกใน
FitUP เดินตาม
Compensate a prior stock sale — e.g. your order failed after the deduction. The
exact lots are restored; idempotent, so a second reverse returns
"reversed": false. No body needed.
คืนสต็อกที่ตัดไปก่อนหน้า เช่น ออร์เดอร์ของคุณล้มเหลวหลังตัดสต็อกไปแล้ว —
ระบบคืนเข้าล็อตเดิมทุกล็อต และเป็น idempotent ดังนั้นเรียกซ้ำจะได้
"reversed": false · ไม่ต้องส่ง body
curl -X POST "$BASE/api/stock/sale/SO-1001/reverse" \
-H "Authorization: Bearer $TOKEN"
{ "success": true, "reversed": true, "orderNo": "SO-1001" }
Leadsลูกค้ามุ่งหวัง (Leads)
The interested-service options to offer on your form. Use each
slug in the lead's interestedService array; show the
label.
ตัวเลือก "บริการที่สนใจ" สำหรับใส่ในฟอร์มของคุณ — ใช้ค่า slug
ในอาร์เรย์ interestedService ของ lead และแสดงข้อความจาก label
curl "$BASE/api/interest-service" -H "Authorization: Bearer $TOKEN"
[
{ "slug": "personal-training", "label": "Personal Training" },
{ "slug": "yoga", "label": "Yoga" }
]
Capture an incomplete lead, e.g. from a website or campaign form. Optional
branch-id header (default 1) attaches the lead to a branch. All five
body fields are required.
บันทึกลูกค้ามุ่งหวังที่ยังไม่สมัคร เช่น จากฟอร์มบนเว็บไซต์หรือแคมเปญ ·
header branch-id (ไม่บังคับ ค่าเริ่มต้น 1) ใช้ผูก lead กับสาขา ·
ฟิลด์ทั้งห้าใน body บังคับทั้งหมด
curl -X POST "$BASE/api/leads" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'branch-id: 1' \
-d '{
"firstName": "Nok",
"lastName": "Suksawat",
"phone": "0812345678",
"email": "nok@example.com",
"interestedService": ["personal-training", "yoga"]
}'
{ "success": true }
Credit (wallet)เครดิต (กระเป๋าเงิน)
Credit is the member's prepaid wallet. Both endpoints deduct it and are idempotent
by receiptNo — reuse the same receiptNo on a retry and the wallet is never
charged twice.
เครดิตคือกระเป๋าเงินเติมล่วงหน้าของสมาชิก · ทั้งสอง endpoint เป็นการตัดเครดิต และเป็น
idempotent ด้วย receiptNo — ใช้ receiptNo เดิมตอน retry แล้วเครดิตจะไม่ถูก
ตัดซ้ำ
Deduct credit from a member identified by userId.
credit must be ≥ 0. description is optional and lands on the wallet
ledger entry.
ตัดเครดิตจากสมาชิกที่ระบุด้วย userId · credit
ต้องมีค่า ≥ 0 · description ไม่บังคับ และจะถูกบันทึกลงรายการเดินบัญชีของกระเป๋าเงิน
curl -X POST "$BASE/api/pay-with-credit" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"userId":123,"credit":150,"receiptNo":"RCPT-1001","description":"POS sale"}'
{ "success": true }
Deduct credit from the member identified by a scanned QR payload.
The QR is single-use — its nonce is consumed on first redemption, so the same QR cannot be charged
twice — and the deduction is additionally idempotent by receiptNo.
ตัดเครดิตจากสมาชิกที่ระบุด้วย payload จากการสแกน QR ·
QR ใช้ได้ครั้งเดียว (nonce ถูกใช้ไปเมื่อแลกครั้งแรก จึงไม่สามารถตัดเงินด้วย QR เดิมซ้ำได้)
และการตัดยังเป็น idempotent ด้วย receiptNo อีกชั้นหนึ่ง
curl -X POST "$BASE/api/pay-with-qr" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"payload":"<scanned-qr-string>","credit":150,"receiptNo":"RCPT-1002","description":"POS sale"}'
{ "success": true }
Paymentsการชำระเงิน
Cash payments through ChillPay: the Payment Gateway (QR PromptPay, card,
installments), hosted PayLinks, and recurring subscriptions. Amounts are in baht. Every create is
idempotent by orderNo, and tenantId is always taken from your token — any
value you send for it is ignored.
การรับชำระเงินสดผ่าน ChillPay: Payment Gateway (QR PromptPay, บัตร, ผ่อนชำระ),
ลิงก์ชำระเงิน (PayLink) และการตัดรอบอัตโนมัติ (recurring) · จำนวนเงินเป็นบาท · ทุกการสร้างเป็น
idempotent ด้วย orderNo และ tenantId จะถูกกำหนดจาก token ของคุณเสมอ
ค่าที่ส่งมาจะถูกละเว้น
Create a one-time Payment Gateway transaction and redirect the customer to the
returned paymentUrl. Required: branchId, orderNo,
amount.
สร้างรายการชำระเงินแบบครั้งเดียว แล้วพาลูกค้าไปที่ paymentUrl
ที่ได้กลับมา · ฟิลด์บังคับ: branchId, orderNo, amount
| Fieldฟิลด์ | Notesหมายเหตุ |
|---|---|
channel |
card · qr_promptpay · installment_kbank ·
installment_scb · installment_krungsri ·
installment_ktc_flexi · installment_firstchoice
(a raw ChillPay code is also accepted).
(ส่งรหัสช่องทางของ ChillPay ตรง ๆ ก็ได้)
|
creditMonth | Required for installment channels — the number of months. บังคับสำหรับช่องทางผ่อนชำระ — จำนวนเดือน |
shopId |
Installments only: "01" = the merchant pays the interest,
"02" = the customer does. Default "01".
ใช้กับผ่อนชำระเท่านั้น: "01" = ร้านรับภาระดอกเบี้ย,
"02" = ลูกค้ารับภาระ · ค่าเริ่มต้น "01"
|
langCode |
TH (default) or EN — the language of the hosted
payment page.
TH (ค่าเริ่มต้น) หรือ EN — ภาษาของหน้าชำระเงิน
|
customerId · customerEmail · phoneNumber · description | Optional; carried onto the transaction for reconciliation. ไม่บังคับ — จะถูกบันทึกไว้กับรายการเพื่อใช้กระทบยอด |
curl -X POST "$BASE/api/payments" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"branchId": 1,
"orderNo": "ORD-1001",
"customerId": "CUST-9",
"amount": 89.92,
"currency": "THB",
"channel": "qr_promptpay",
"customerEmail": "cust@example.com",
"langCode": "TH"
}'
{
"orderNo": "ORD-1001",
"transactionId": 550231,
"channelCode": "bank_qrcode",
"paymentUrl": "https://sandbox-cashier.chillpay.co/...",
"expiredDate": "2026-07-28T10:15:00.000Z"
}
Poll a payment's status with the transactionId from the create
call. Returns the transaction as recorded by the payment service; 404 when the id is
unknown to your tenant.
ตรวจสถานะการชำระเงินด้วย transactionId ที่ได้จากตอนสร้างรายการ ·
ผลลัพธ์คือรายการตามที่บริการชำระเงินบันทึกไว้ และจะได้ 404 หากไม่พบ id นั้นใน
tenant ของคุณ
curl "$BASE/api/payments/550231/status" -H "Authorization: Bearer $TOKEN"
Responseการตอบกลับ · 200
{
"transactionId": 550231,
"orderNo": "ORD-1001",
"paymentStatus": 0,
"statusText": "Complete",
"bankCode": "SCB",
"bankRefCode": "0043215",
"amount": 250.00,
"currency": 764
}
Status codesรหัสสถานะ
| paymentStatuspaymentStatus | statusTextstatusText | Final?สิ้นสุด? | Meaningความหมาย |
|---|---|---|---|
9 | Pending | no — keep pollingยัง — ให้ poll ต่อ | Awaiting the customer's payment.รอลูกค้าชำระเงิน |
0 | Complete | yesใช่ | Paid. Fulfil the order.ชำระเงินสำเร็จ — ส่งมอบสินค้า/บริการได้ |
23 | Settled | yesใช่ | Paid and settled to the merchant — treat exactly like 0.ชำระเงินแล้วและโอนเข้าบัญชีร้านค้าแล้ว — ถือว่าสำเร็จเช่นเดียวกับ 0 |
1 | Fail | yesใช่ | Rejected (e.g. by the issuer). Ask the customer to try again with a new order.ถูกปฏิเสธ (เช่น จากธนาคารผู้ออกบัตร) — ให้ลูกค้าลองใหม่ด้วยรายการใหม่ |
2 | Cancel | yesใช่ | The customer abandoned the payment page.ลูกค้ายกเลิก/ออกจากหน้าชำระเงิน |
3 | Error | yesใช่ | The payment could not be processed. Do not fulfil; start a new order.ประมวลผลการชำระเงินไม่สำเร็จ — อย่าส่งมอบสินค้า ให้เริ่มรายการใหม่ |
20 / 24 | VoidSuccess / VoidFail | yesใช่ | Outcome of a void requested by the merchant.ผลของการยกเลิกรายการ (void) ที่ร้านค้าร้องขอ |
21 / 25 | RefundSuccess / RefundFail | yesใช่ | Outcome of a refund.ผลของการคืนเงิน (refund) |
22 | RefundRequested | noยัง | A refund is being processed.อยู่ระหว่างดำเนินการคืนเงิน |
0 and 23 mean the money is yours — never fulfil an
order on any other code. statusText is a convenience label derived from
paymentStatus; branch on the number, and treat an unrecognised code
as not-paid (statusText will read Unknown). While polling a
Pending payment, a few seconds between calls is plenty — the customer still has to
finish on the bank's page.
มีเพียง 0 และ 23 ที่หมายถึงได้รับเงินแล้ว — อย่าส่งมอบสินค้า
เมื่อได้รหัสอื่น · statusText เป็นเพียงข้อความอธิบายที่แปลงมาจาก paymentStatus
ให้ตัดสินใจจากตัวเลข และหากพบรหัสที่ไม่รู้จักให้ถือว่ายังไม่ชำระเงิน
(statusText จะเป็น Unknown) · ระหว่าง poll สถานะ Pending
เว้นระยะไม่กี่วินาทีต่อครั้งก็เพียงพอ เพราะลูกค้ายังต้องทำรายการบนหน้าเว็บของธนาคารให้เสร็จก่อน
Create a hosted payment link (and QR image) you can send to a customer.
Required: orderNo, productName, amount. Optional:
branchId, productDescription (defaults to productName),
startDate (default now), expiredDate (default +24h),
paymentLimit (default 1; 0 = unlimited).
สร้างลิงก์ชำระเงิน (พร้อมภาพ QR) เพื่อส่งให้ลูกค้า · ฟิลด์บังคับ:
orderNo, productName, amount · ไม่บังคับ:
branchId, productDescription (ค่าเริ่มต้นเท่ากับ productName),
startDate (ค่าเริ่มต้น = ตอนนี้), expiredDate (ค่าเริ่มต้น +24 ชม.),
paymentLimit (ค่าเริ่มต้น 1; ใส่ 0 = ไม่จำกัดจำนวนครั้ง)
curl -X POST "$BASE/api/paylink" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"branchId": 1,
"orderNo": "PL-2001",
"productName": "Monthly membership",
"amount": 1290.00,
"currency": "THB",
"expiredDate": "2026-08-01T17:00:00Z",
"paymentLimit": 1
}'
{
"orderNo": "PL-2001",
"payLinkId": 88134,
"paymentUrl": "https://sandbox-paylink.chillpay.co/...",
"qrImage": "https://sandbox-paylink.chillpay.co/qr/...",
"status": "ACTIVE"
}
Fetch one PayLink transaction — whether it was paid, when, and for how much. ดึงข้อมูลรายการของ PayLink หนึ่งรายการ — ชำระแล้วหรือไม่ ชำระเมื่อไร และเป็นจำนวนเท่าไร
curl "$BASE/api/paylink/transactions/88134" -H "Authorization: Bearer $TOKEN"
Create a subscription schedule. Required: orderNo,
amount (per cycle), billingDueDate, billingCycle
(day | month | year) and billingRound (the
interval multiplier, e.g. 1). Omit totalBilling for an open-ended
subscription. Send the customer to verifyPaymentUrl once to register their
card.
สร้างรอบตัดเงินอัตโนมัติ (subscription) · ฟิลด์บังคับ: orderNo,
amount (ต่อรอบ), billingDueDate, billingCycle
(day | month | year) และ billingRound
(ตัวคูณช่วงเวลา เช่น 1) · ไม่ใส่ totalBilling = ตัดเรื่อยไปไม่จำกัดรอบ ·
พาลูกค้าไปที่ verifyPaymentUrl หนึ่งครั้งเพื่อผูกบัตร
curl -X POST "$BASE/api/recurring/schedules" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"branchId": 1,
"customerId": "CUST-9",
"customerEmail": "cust@example.com",
"orderNo": "SUB-3001",
"amount": 1290.00,
"currency": "THB",
"description": "Gold membership",
"billingDueDate": "2026-08-01",
"billingCycle": "month",
"billingRound": 1,
"totalBilling": 12
}'
{
"recurringScheduleId": "b7c1f0d2-4a55-4e9b-9c31-0a8d7e6f5b42",
"verifyPaymentUrl": "https://sandbox-cashier.chillpay.co/verify/...",
"status": "PENDING_VERIFY"
}
Fetch a schedule's current state — verification status, next billing date and rounds billed so far. ดึงสถานะปัจจุบันของรอบตัดเงิน — สถานะการยืนยันบัตร, วันตัดรอบถัดไป และจำนวนรอบที่ตัดไปแล้ว
curl "$BASE/api/recurring/schedules/b7c1f0d2-4a55-4e9b-9c31-0a8d7e6f5b42" \
-H "Authorization: Bearer $TOKEN"
Stop future billing for a schedule. The body is optional:
{ "reason": "..." } is stored with the cancellation. Already-billed rounds are not
refunded.
หยุดการตัดเงินรอบถัดไปของ schedule นั้น · body ไม่บังคับ — ส่ง
{ "reason": "..." } เพื่อบันทึกเหตุผลไว้กับการยกเลิก · รอบที่ตัดเงินไปแล้วจะไม่ถูกคืนเงิน
curl -X POST "$BASE/api/recurring/schedules/b7c1f0d2-4a55-4e9b-9c31-0a8d7e6f5b42/cancel" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"reason":"customer request"}'
System & docsระบบและเอกสาร
These three routes need no token — they are served by the gateway itself. สาม route นี้ไม่ต้องใช้ token — เกตเวย์เป็นผู้ให้บริการเองโดยตรง
| RouteRoute | Purposeใช้ทำอะไร |
|---|---|
GET /health |
Liveness probe — {"status":"ok"}. Use it for uptime checks, not as
a dependency health report.
ตรวจว่าบริการยังมีชีวิต — ตอบ {"status":"ok"} · ใช้สำหรับเช็ก uptime
ไม่ใช่รายงานสุขภาพของบริการเบื้องหลัง
|
GET /doc.html |
This page. GET / redirects here.
หน้านี้เอง · เรียก GET / จะถูกส่งต่อมาที่นี่
|
GET /openapi.yaml | The OpenAPI 3.1 spec for this exact deployment — feed it to your client generator or Postman. สเปก OpenAPI 3.1 ของเวอร์ชันที่ deploy อยู่จริง — นำไปสร้าง client หรือ import เข้า Postman ได้ |
Quick start (curl)เริ่มใช้เร็ว (curl)
Set BASE to your host, get a token, then work through a branch, the
catalog and a sale. Every example above uses these two shell variables.
ตั้งค่า BASE เป็นโฮสต์ของคุณ, ขอ token, แล้วไล่ตามลำดับ: เลือกสาขา,
ดูแคตตาล็อก, บันทึกการขาย · ตัวอย่างทั้งหมดด้านบนใช้ตัวแปรเชลล์สองตัวนี้
BASE="https://<your-open-api-host>"
# 1) Token (cache it — valid for expires_in seconds)
TOKEN=$(curl -s -X POST "$BASE/api/oauth/token" \
-H 'Content-Type: application/json' \
-d '{"clientId":"YOUR_CLIENT_ID","clientSecret":"YOUR_CLIENT_SECRET"}' | jq -r .access_token)
# 2) Which branches can I sell at?
curl -s "$BASE/api/branches" -H "Authorization: Bearer $TOKEN"
# 3) Catalog for branch 2, with on-hand stock attached
curl -s "$BASE/api/products?pageSize=50" \
-H "Authorization: Bearer $TOKEN" -H 'branch-id: 2'
# 4) Record a sale (deducts stock + credit, writes the receipt)
# No cashierId -> credited to the fitness's default cashier
curl -s -X POST "$BASE/api/purchases" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-H 'branch-id: 2' -H 'X-Correlation-Id: my-trace-0001' \
-d '{"items":[{"productId":"<product-id>","qty":2}],"paymentType":"CASH"}'
# 5) Or take a QR PromptPay payment instead
curl -s -X POST "$BASE/api/payments" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"branchId":2,"orderNo":"ORD-1001","amount":89.92,"channel":"qr_promptpay"}'
Changesสิ่งที่เปลี่ยนแปลง
What changed on this API, newest first. Dates are when the change went live on our side. This page and openapi.yaml are served by the deployment you are calling, so they always describe the behaviour you actually get. รายการสิ่งที่เปลี่ยนแปลงของ API นี้ เรียงจากใหม่ไปเก่า · วันที่คือวันที่การเปลี่ยนแปลงนั้น ขึ้นใช้งานฝั่งเรา · หน้านี้และ openapi.yaml ให้บริการจาก deployment ตัวเดียวกับที่คุณเรียกอยู่ จึงตรงกับพฤติกรรมที่คุณได้รับจริงเสมอ
| Dateวันที่ | Changeการเปลี่ยนแปลง |
|---|---|
| 2026-10-14 |
Added — the receipt.branch object on
POST /api/purchases now carries isHeadOffice and label, the branch
line exactly as FitUP prints it (สำนักงานใหญ่ for the head office, else
สาขา {name} ({code})). Print label in place of your own name + code line.
A branch can now also issue receipts under its own company name, tax id and address; those arrive in
the same fitness / branch fields, so nothing else changes for you. See
Purchases.
เพิ่ม — ออบเจ็กต์ receipt.branch ของ
POST /api/purchases มี isHeadOffice และ label เพิ่มมา คือบรรทัดสาขา
ตามที่ FitUP พิมพ์จริง (สำนักงานใหญ่ สำหรับสำนักงานใหญ่ นอกนั้นเป็น
สาขา {name} ({code})) · ให้พิมพ์ label แทนบรรทัดชื่อ + รหัสสาขาที่ประกอบเอง ·
ตอนนี้สาขาออกใบเสร็จในนามบริษัท เลขผู้เสียภาษี และที่อยู่ของตัวเองได้ด้วย ค่าพวกนี้มาในฟิลด์
fitness / branch เดิม ฝั่งคุณจึงไม่ต้องแก้อะไรเพิ่ม · ดู
การขาย
|
| 2026-09-09 |
Fixed — POST /api/stock/sale no longer rejects an
order that contains catalog-only products. A product with
"trackStock": false has no on-hand by design and is always sellable; its line now
moves no stock and comes back in the new untracked array on the response instead of
failing the whole call with 409 insufficient stock. You do not need to know which
products the club tracks before selling them.Changed — a genuine 409 now names the product that ran short:
insufficient stock for product <productId>. See
Stock.
แก้ไข — POST /api/stock/sale จะไม่ปฏิเสธออร์เดอร์
ที่มีสินค้าซึ่งไม่นับสต็อกอีกต่อไป · สินค้าที่ "trackStock": false ไม่มียอดคงเหลือ
โดยการออกแบบและขายได้เสมอ ดังนั้นบรรทัดนั้นจะไม่ตัดสต็อกและจะถูกส่งกลับใน array ใหม่
untracked แทนที่จะทำให้ทั้งคำขอล้มเหลวด้วย 409 insufficient stock ·
คุณไม่จำเป็นต้องรู้ล่วงหน้าว่าสินค้าใดของคลับนับสต็อกอยู่เปลี่ยน — กรณีสต็อกไม่พอจริง 409 จะระบุสินค้าที่ไม่พอด้วย:
insufficient stock for product <productId> · ดู
สต็อก
|
| 2026-08-05 |
Added — POST /api/purchases now returns a
receipt object with everything printed on a FitUP receipt (club and branch header, line
items, VAT breakdown, Thai baht text), so your POS can lay out the same slip we do. See
Purchases.Changed — a product can now be sold at several branches, so GET /api/products returns branchIds (an array; [] = every
branch). branchId is deprecated: it still comes back, but only holds an id when
the product sits on exactly one branch — it is null for an all-branch and a
multi-branch product. Read branchIds.
เพิ่ม — POST /api/purchases ตอบออบเจ็กต์
receipt ที่มีข้อมูลทุกอย่างบนใบเสร็จของ FitUP (หัวใบเสร็จของฟิตเนสและสาขา, รายการสินค้า,
การแยก VAT, ตัวหนังสือจำนวนเงินภาษาไทย) เพื่อให้ POS ของคุณจัดสลิปได้เหมือนกับที่เราออก · ดู
การขายเปลี่ยน — สินค้า 1 ตัวขายได้หลายสาขาแล้ว ดังนั้น GET /api/products จะส่ง branchIds มา (เป็น array · []
หมายถึงขายทุกสาขา) · branchId กลายเป็น deprecated — ยังส่งมาให้ แต่จะมีค่าเฉพาะ
กรณีที่สินค้าผูกกับสาขาเดียว และเป็น null ทั้งกรณีขายทุกสาขาและกรณีขายหลายสาขา
จึงควรอ่าน branchIds
|
| 2026-08-04 |
Added — optional posId on
POST /api/purchases: the serial of the POS machine ringing up the sale, as printed on the
machine. It stamps that machine on the receipt and switches the tax-invoice number to the machine's own
series. Omit it and the branch's own series is used, exactly as before. An unregistered serial is
rejected with 400 POS_NOT_FOUND.
เพิ่ม — ฟิลด์ posId (ไม่บังคับ) ใน
POST /api/purchases คือหมายเลขเครื่อง POS ที่ใช้ขาย ตามที่พิมพ์อยู่บนเครื่อง ·
ระบบจะบันทึกเครื่องนั้นบนใบเสร็จและใช้ชุดเลขใบกำกับภาษีของเครื่องนั้น · ถ้าไม่ส่งมาจะใช้ชุดเลขของสาขา
เหมือนเดิมทุกอย่าง · หากส่งหมายเลขที่ไม่ได้ลงทะเบียนไว้จะได้ 400 POS_NOT_FOUND
|
| 2026-07-31 |
Changed — cashierId on
POST /api/purchases became optional: omit it and the sale is credited to
the fitness's default cashier, so you never need FitUP's internal staff ids. Send it only to override
that for one sale.Changed — every request and response field on this API is now camelCase. Member fields were renamed ( member_code →
memberCode, credit_balance → creditBalance,
image_url → imageUrl) and the product filter
?payment_type= became ?paymentType=. The OAuth token response keeps its
RFC 6749 names (access_token, token_type, expires_in) and
ChillPay channel values are unchanged. ?payment_type= is still accepted.
เปลี่ยน — cashierId ใน
POST /api/purchases กลายเป็น ไม่บังคับ · ถ้าไม่ส่ง ระบบจะบันทึกการขาย
ให้พนักงานเริ่มต้นของฟิตเนสนั้น คุณจึงไม่จำเป็นต้องรู้รหัสพนักงานภายในของ FitUP · ส่งมาเมื่อต้องการ
ระบุเป็นรายครั้งเท่านั้นเปลี่ยน — ฟิลด์ทุกตัวทั้ง request และ response ของ API นี้เป็น camelCase ทั้งหมด · ฟิลด์ของสมาชิกถูกเปลี่ยนชื่อ ( member_code →
memberCode, credit_balance → creditBalance,
image_url → imageUrl) และตัวกรองสินค้า ?payment_type=
เปลี่ยนเป็น ?paymentType= · ส่วน response ของ OAuth token ยังใช้ชื่อตาม RFC 6749
(access_token, token_type, expires_in) และค่า channel ของ
ChillPay ไม่เปลี่ยน · ?payment_type= ยังใช้ได้อยู่
|
| 2026-07-29 |
Added — your token now carries the fitness's VAT settings, and
GET /api/me reports them (vatRate,
isVatRegistered), so you can price a cart on your side without a call per line item.
vatRate is the effective rate — 0 when the fitness is not
VAT-registered. Because it is baked into the token, a rate change reaches you when you next request a
token.Added — POST /api/purchases returns vat and
vatRate for the sale. Prices are VAT-inclusive: vat is the portion already
contained in total.
เพิ่ม — token ของคุณมีค่า VAT ของฟิตเนสติดมาด้วย และ
GET /api/me แสดงค่าเหล่านั้น (vatRate,
isVatRegistered) ทำให้คุณคำนวณราคาตะกร้าฝั่งคุณได้เองโดยไม่ต้องยิงถามทีละรายการ ·
vatRate เป็นอัตราที่ใช้จริง — จะเป็น 0 ถ้าฟิตเนสไม่ได้จด VAT ·
เนื่องจากค่านี้ฝังอยู่ใน token การแก้อัตราจะมีผลกับคุณเมื่อขอ token ใหม่ครั้งถัดไปเพิ่ม — POST /api/purchases ส่ง vat และ
vatRate ของการขายนั้นกลับมา · ราคารวม VAT อยู่แล้ว ดังนั้น vat คือส่วนที่
รวมอยู่ใน total แล้ว
|
| 2026-07-27 |
Added — GET /api/branches
(which branch ids you may sell at; no extra scope) and
POST /api/purchases, which records a whole sale in one call:
it validates products, deducts stock and the member's credit wallet, and writes the receipt. Do not
pair it with /api/stock/sale or /api/pay-with-credit for the same sale —
that double-deducts.Added — products carry categoryId and
GET /api/product-categories lists the categories (filter with
?categoryId=).
เพิ่ม — GET /api/branches
(ดูว่าขายที่สาขาไหนได้ ไม่ต้องใช้ scope เพิ่ม) และ
POST /api/purchases ที่บันทึกการขายจบในครั้งเดียว —
ตรวจสอบสินค้า, ตัดสต็อก, ตัดเครดิตของสมาชิก และออกใบเสร็จ · อย่าเรียกคู่กับ
/api/stock/sale หรือ /api/pay-with-credit สำหรับการขายเดียวกัน
เพราะจะถูกตัดสองครั้งเพิ่ม — สินค้ามีฟิลด์ categoryId และมี
GET /api/product-categories ให้ดูรายการหมวดหมู่ (กรองด้วย
?categoryId=)
|
| 2026-07-18 |
Added — the Stock section: on-hand per
branch (GET /api/stock, GET /api/products/{id}/stock), lot visibility in
consume order (GET /api/stock/lots, GET /api/stock/expiring) and the
stock-first sale flow (POST /api/stock/sale plus its reversal), under the new
read:stock / write:stock scopes. Sending the branch-id header
to GET /api/products also attaches that branch's on-hand to every row.
เพิ่ม — หมวด สต็อก ทั้งหมด: ยอดคงเหลือ
แยกตามสาขา (GET /api/stock, GET /api/products/{id}/stock), การดูล็อตตาม
ลำดับการตัด (GET /api/stock/lots, GET /api/stock/expiring) และการตัดสต็อก
แบบ stock-first (POST /api/stock/sale และการย้อนกลับ) ภายใต้ scope ใหม่
read:stock / write:stock · และถ้าส่ง header branch-id ไปกับ
GET /api/products ทุกรายการจะมียอดคงเหลือของสาขานั้นแนบมาด้วย
|
| 2026-06-27 |
Added — the Payments section: card and
QR PromptPay charges (POST /api/payments), PayLink and recurring schedules, with status
reads under read:payments. Only paymentStatus 0 (Complete) and
23 (Settled) mean the money is collected — never fulfil an order on any other
code.
เพิ่ม — หมวด ชำระเงิน: รับชำระด้วยบัตร
และ QR PromptPay (POST /api/payments), PayLink และการตัดเงินแบบรายรอบ (recurring)
พร้อมการอ่านสถานะภายใต้ read:payments · มีเพียง paymentStatus
0 (Complete) และ 23 (Settled) ที่หมายถึงได้รับเงินแล้ว — อย่าส่งของหรือ
ยืนยันคำสั่งซื้อจากโค้ดอื่น
|
| 2026-06-25 |
Changed — the member's prepaid wallet is called
credit everywhere (it was "points"): scope write:credit, endpoints
POST /api/pay-with-credit and POST /api/pay-with-qr, field
creditBalance, body field credit. Both wallet deductions are
idempotent by receiptNo — a retry with the same
receiptNo does not deduct twice.
เปลี่ยน — กระเป๋าเงินเติมล่วงหน้าของสมาชิกเรียกว่า
เครดิต (credit) ทั้งหมด (เดิมคือ "points"): scope write:credit,
endpoint POST /api/pay-with-credit และ POST /api/pay-with-qr, ฟิลด์
creditBalance, ฟิลด์ใน body ชื่อ credit · การตัดเครดิตทั้งสองเส้นเป็น
idempotent ด้วย receiptNo — ยิงซ้ำด้วย receiptNo เดิม
จะไม่ถูกตัดสองครั้ง
|