← หน้าแรก Read in English →
Tang La
ตังค์ละ?

API ภายนอก

ตังค์ละเปิด HTTP API เล็ก ๆ ในเครื่องได้ ให้สคริปต์ หรืออุปกรณ์อื่นในวง Wi-Fi อ่านยอดเงินและเพิ่มรายการได้ โดยไม่ต้องเปิดแอป หน้านี้คือเอกสารฉบับเต็ม (ถ้าแค่อยากเพิ่มทีละรายการจาก Shortcut/QR/NFC ตอนถือเครื่องอยู่ ใช้ลิงก์ทางลัดแทนจะง่ายกว่า — ดูรายละเอียดด้านล่าง)

01

เปิดใช้งาน

อยากจัดการจากคอมพิวเตอร์โดยไม่ต้องเขียนโค้ด? ใช้ tangla-manage

tangla-manage เป็นหน้าเว็บสำเร็จรูปที่ต่อกับตังค์ละในวง Wi-Fi เดียวกัน ใช้ชื่อผู้ใช้/รหัสผ่านที่ตั้งในแอป (ดูขั้นตอนด้านล่าง) ดู เพิ่ม แก้ไข และลบรายการ บัญชี หมวดหมู่ แท็ก งบประมาณ และรายการประจำได้จากเบราว์เซอร์เลย ไม่ต้องยิง API เอง ส่วนที่เหลือของหน้านี้สำหรับคนที่จะเขียนสคริปต์หรือเครื่องมือของตัวเอง

แค่เพิ่มทีละรายการตอนถือเครื่องอยู่? ใช้ลิงก์ทางลัดแทน

ถ้าต้องการแค่เพิ่มรายการเดียวจากสิ่งที่ต้องถือเครื่องอยู่แล้ว — Shortcut บนหน้าโฮม, สแกน QR, แตะ NFC tag — ใช้ลิงก์ tangla://add?… แทนหน้านี้จะง่ายกว่ามาก ไม่ต้องเปิด server, ไม่ต้องมีโทเค็น, ไม่ต้องยอมรับใบรับรอง และทำงานได้แม้ปิดแอปอยู่ เปิดได้ที่ในแอป ตั้งค่า → การเชื่อมต่อภายนอก → "รับรายการจากลิงก์ทางลัด" (ฟีเจอร์พรีเมียม, มีรูปแบบลิงก์เต็มในหน้านั้น)

ใช้ HTTP API ในหน้านี้ต่อเมื่อต้องเพิ่มหลายรายการพร้อมกัน (เช่น import จากไฟล์ statement) หรือสั่งจากอุปกรณ์อื่นที่ไม่ใช่เครื่องนี้ — สองอย่างนี้ลิงก์ทางลัดทำไม่ได้

ในตังค์ละ ไปที่ ตั้งค่า → การเชื่อมต่อภายนอก → "API ภายนอก" แล้วเปิด "เปิดใช้งาน API" ถ้ายังไม่เคยตั้ง ชื่อผู้ใช้/รหัสผ่านมาก่อน แอปจะถามให้ตั้งทันที — เพราะไม่มีอะไรใช้ได้เลย ถ้าไม่มีบัญชีสำหรับเข้าสู่ระบบ (ดู การยืนยันตัวตน) กดยกเลิกแล้วมาตั้งทีหลังจาก การ์ดด้านล่างก็ได้ เซิร์ฟเวอร์ยังทำงานต่อ หน้าจอจะแสดง:

  • ที่อยู่เซิร์ฟเวอร์ — ค่าเดียวรูปแบบ https://<ip>:<port> พอร์ตสุ่มครั้งเดียว แล้วใช้ค่าเดิมทุกครั้งที่เปิด จึงคัดลอกเข้าเครื่องมือแค่ครั้งเดียว ถ้าใช้ขอบเขต LAN บน Android และเครือข่ายรองรับ mDNS จะโชว์ https://tangla.local:<port> เพิ่มด้วย — ชื่อคงที่ที่ยังใช้ได้แม้ Wi-Fi เปลี่ยน IP จึงยืนยันใบรับรอง ครั้งเดียวจบ ถ้าเครือข่ายไม่รองรับก็ใช้ IP ตามเดิม
  • เข้าสู่ระบบผ่านเว็บ — การ์ดตั้ง/เปลี่ยน/ลบ ชื่อผู้ใช้และรหัสผ่าน

เซิร์ฟเวอร์ทำงานเฉพาะตอนเปิดแอปค้างไว้ที่หน้าจอเท่านั้น — หน้าจอมือถือดับหรือสลับไปแอปอื่นเมื่อไหร่ เซิร์ฟเวอร์ก็หยุดตอบทันที ไม่ต้องรอให้ระบบปิดแอปสนิทก่อน (ตั้งใจไม่ให้ทำงานเป็น background service) จะเริ่มใหม่เองตอนเปิดแอปครั้งถัดไปถ้าเคยเปิดใช้งานไว้ ถ้าจะเรียก API แบบไม่มีคนเฝ้าหน้าจอ (เช่น shortcut/automation ที่ตั้งเวลา) ต้องเปิดแอปค้างไว้และกันจอไม่ให้ดับตลอดช่วงนั้นด้วย ตั้งได้ในหน้าจอเดียวกัน ตามด้านล่าง

กันจอไม่ให้ดับ

สวิตช์ "ให้หน้าจอค้างไว้" ทำให้จอไม่ดับตอนเซิร์ฟเวอร์เปิดอยู่ และแอปอยู่หน้าจอ — สลับไปแอปอื่นเมื่อไหร่ จอก็ดับตามปกติ และเซิร์ฟเวอร์หยุดตอบเหมือนเดิม การค้างหน้าจอสิ้นเปลืองแบตเตอรี่ จึงแนะนำให้เสียบสายชาร์จไว้ เมื่อเปิดสวิตช์นี้จะมีอีกสองตัวเลือกให้ตั้ง เพื่อให้ไม่ต้องค้างจอสว่างเต็มที่ตลอดเวลา:

  • หรี่หน้าจอเมื่อไม่ได้แตะ — จอยังติดอยู่แต่มืดลง เพื่อประหยัดแบต เลือกได้ 15 วินาที, 30 วินาที (ค่าเริ่มต้น), 1 นาที หรือไม่หรี่เลย แตะที่ไหนก็ได้เพื่อให้กลับมาสว่างเท่าเดิม โดยการแตะครั้งแรกจะไม่กดปุ่มที่อยู่ใต้นิ้ว
  • ปล่อยให้จอดับเมื่อไม่มีการใช้งานเว็บ — ถ้าไม่มีคำขอเข้ามาที่เซิร์ฟเวอร์นานเท่าที่ตั้งไว้ แอปจะเลิกค้างหน้าจอ และปล่อยให้ดับตามปกติ (แปลว่าเซิร์ฟเวอร์หยุดตอบด้วย) เลือกได้ 5, 15 (ค่าเริ่มต้น), 30, 60 นาที หรือไม่ปล่อยเลย
02

HTTPS กับใบรับรอง self-signed

เซิร์ฟเวอร์ใช้ HTTPS และใบรับรองเป็นแบบ self-signed (สร้างในเครื่องตอนใช้ครั้งแรก) ดังนั้น:

  • เบราว์เซอร์จะเตือนว่า "ไม่น่าเชื่อถือ" — กด ขั้นสูง → ไปต่อ ครั้งเดียวต่อ IP:PORT ถ้า Wi-Fi เปลี่ยนแล้ว IP เปลี่ยนไป ต้องยืนยันใหม่
  • curl ต้องใส่ -k (insecure)
  • ควรใช้เฉพาะเครือข่ายบ้าน/ในวงที่เชื่อถือได้

แอปแสดง ลายนิ้วมือ SHA-256 ของใบรับรองไว้ในหน้า "API ภายนอก" — เปิดที่อยู่เครื่องโดยตรงในแท็บใหม่ แล้วเทียบกับ ค่า SHA-256 ในรายละเอียดใบรับรองที่เบราว์เซอร์แสดงตรงนั้น เพื่อยืนยันว่าต่อถึงเครื่องที่ถูกต้อง ไม่มีตัวกลางในวง LAN สวม ใบรับรองปลอม (ไม่ใช่ใบรับรองของหน้า tangla-manage เอง เพราะเป็น ใบรับรองของเว็บ คนละใบกัน) ค่าเดียวกันนี้เรียกผ่าน GET /v1/server/fingerprint ได้ (ไม่ต้องแนบโทเค็น — ต้องเช็คได้ก่อนล็อกอินเพื่อยืนยันว่าไม่มีตัวกลางดักอยู่)

03

ขอบเขตการเข้าถึง

ตั้งในหน้าจอเดียวกัน เซิร์ฟเวอร์จะสร้างใหม่เมื่อเปลี่ยนค่า

ขอบเขต Bind ที่ เข้าถึงได้จาก
เฉพาะเครื่องนี้ 127.0.0.1 เท่านั้น เครื่องมือในเครื่องนี้เท่านั้น — adb reverse หรือแอป/สคริปต์อัตโนมัติในเครื่อง อุปกรณ์อื่นเข้าไม่ได้แม้มีโทเค็น
ทั้งวง Wi-Fi/LAN 0.0.0.0 อุปกรณ์ใดก็ได้ในวง Wi-Fi/LAN เดียวกันที่มีโทเค็นที่ถูกต้อง

ขอบเขต "เฉพาะเครื่องนี้" ที่อยู่ที่แอปแสดงคือ 127.0.0.1 ส่วน "ทั้งวง Wi-Fi/LAN" จะเป็น IP ของ Wi-Fi เครื่องนั้น

04

การยืนยันตัวตน

มีโทเค็นแบบเดียว: POST /v1/auth/login ด้วยชื่อผู้ใช้/รหัสผ่านที่ตั้งในแอป จะได้ bearer JWT (อายุ 60 นาที ไม่มี refresh — หมดอายุแล้วก็ login ใหม่) ทุก request ที่เหลือ รวมถึง /v1/health ต้องแนบโทเค็นนี้:

Authorization: Bearer <token>

ไม่มีโทเค็นหรือโทเค็นผิด → 401. POST /v1/auth/login เองเรียกได้โดยไม่ต้องมีโทเค็น (เพราะเป็นทางเดียวที่จะได้โทเค็นมา) แต่ถ้ายังไม่เคยตั้ง ชื่อผู้ใช้/รหัสผ่านในแอป จะได้ 403 {"error":"Web sign-in is not configured"}

05

รายการ route ทั้งหมด

รายการ route ทั้งหมดที่เข้าถึงได้หลัง login (ดู การยืนยันตัวตน) ตั้ง ชื่อผู้ใช้ + รหัสผ่าน ได้ที่ ตั้งค่า → การเชื่อมต่อภายนอก → "API ภายนอก" (การ์ด "เข้าสู่ระบบผ่านเว็บ") JWT ที่ได้ใช้กับขอบเขต LAN ได้ด้วย เพราะสร้างจากรหัสผ่านของคุณเอง และมีวันหมดอายุ จึงปลอดภัยกว่าโทเค็นคงที่ หน้าเว็บคู่กันคือ tangla-manage.meowmeow-studio.com

เข้าสู่ระบบและโทเค็น

POST /v1/auth/login body {"username","password"} → {"token","tokenType":"Bearer","expiresIn":3600} เป็น HS256 JWT อายุ 60 นาที ไม่มี refresh — เจอ 401 ให้ login ใหม่ 403 ถ้ายังไม่ตั้งชื่อผู้ใช้/รหัสผ่าน 401 ถ้าชื่อผู้ใช้/รหัสผ่านผิด 429 พร้อม Retry-After หลังผิด 5 ครั้ง การเปลี่ยนหรือลบชื่อผู้ใช้/รหัสผ่านทำให้ JWT ทุกตัวใช้ไม่ได้ หลังเซิร์ฟเวอร์รีสตาร์ทครั้งถัดไป

ส่ง Authorization: Bearer <jwt> ไปที่:

Route หมายเหตุ
GET /v1/entries ?from=&to= (epoch ms) หรือ ?cycle= หรือ ?limit= (ค่าเริ่มต้น 200 สูงสุด 1000) → {"entries":[…],"count":N} ทุก entry มี tags:[id]
POST /v1/entries สร้างรายการเดียว body เหมือนสมาชิก entries[] หนึ่งตัว (มี autoCreate, idempotencyKey, tagIds)
PUT /v1/entries/<id> แทนที่ทั้งรายการ — ฟิลด์ที่ไม่ส่งจะถูก ล้าง (title → "", note → null) ส่งทั้งรายการ timestamp รูปใบเสร็จ และที่มาแบบ ประจำ/ผ่อนถูกเก็บไว้ 404 ถ้าไม่มี id นั้น
DELETE /v1/entries/<id> ย้อนผลต่อยอดเงิน ลบเฉพาะรายการนั้นของแผนผ่อน 404 ถ้าไม่มี id นั้น 409 ถ้าเป็นรายการรับ/จ่ายของบัญชีเงินกู้ (ยอดหนี้หรือการปรับยอด ซึ่งแก้ได้ในแอปเท่านั้น)
GET /v1/categories {"income":[…],"expense":[…]} หมวดที่ซ่อนไว้รวมมาด้วย isHidden: true
POST /v1/categories {name, type, colorIndex?, icon?} (type = income/expense) ชื่อซ้ำในประเภท เดียวกัน → 409
PUT /v1/categories/<id> merge: name/colorIndex/icon/isHidden/sortOrder เปลี่ยน type ไม่ได้ เปลี่ยนชื่อ cascade ไป ทุก entry/กฎ/งบ 404/409
DELETE /v1/categories/<id> 409 พร้อม usage ถ้ายังถูกใช้ อยู่ ไม่งั้นลบ
POST /v1/accounts {name, type?, includeInTotal?, includeInNetWorth?, note?} ยอดเริ่ม 0 เสมอ (ไม่มี route ปรับยอด — ทำในแอป) ชื่อซ้ำ → 409
PUT /v1/accounts/<id> merge: name/type/includeInTotal/includeInNetWorth/note/isHidden balance และเงื่อนไขกู้/เป้าหมายถูกเก็บไว้ บัญชีกู้ หรือเปลี่ยน type เป็น/จาก loan → 400
DELETE /v1/accounts/<id> 409 ถ้ามี entry อ้างถึง (ซ่อนแทน) ไม่งั้น soft-delete
GET /v1/tags ทุกแท็ก → [{id,name,tagType,colorIndex,sortOrder}]
POST /v1/tags {name, colorIndex?} เป็น tagType:"custom" เสมอ ชื่อซ้ำ → 409
PUT /v1/tags/<id> merge name/colorIndex/sortOrder 404/409
DELETE /v1/tags/<id> ลบแท็กและทุกแถวที่ใช้แท็กนั้น แท็ก need/want ลบไม่ได้ (400)
GET /v1/budgets ?cycle=<ms> (ค่าเริ่มต้นตอนนี้) → งบ หมวดหมู่ของรอบนั้น {cycle:{key,start,end}, budgets:[…]}
PUT /v1/budgets upsert งบหนึ่งหมวด: {category, monthlyLimit, cycle?, muted?} monthlyLimit ≥ 0, category ต้องเป็นหมวดรายจ่ายที่มีอยู่
DELETE /v1/budgets/<id> 404 ถ้าไม่มีแถวนั้น
GET /v1/recurring ทุกกฎรายการประจำ เรียง nextRun ใกล้สุดก่อน แต่ละอันมี linkedLoan — กฎที่ผูกบัญชีกู้แก้ ที่นี่ไม่ได้
POST /v1/recurring สร้างกฎประจำแบบธรรมดา body เหมือน entry + frequency (daily/weekly/monthly/yearly) + dayOfWeek (1–7 จำเป็นเมื่อ weekly) / dayOfMonth (1–28 จำเป็นเมื่อ monthly และ yearly) / monthOfYear (1–12 จำเป็นเมื่อ yearly) และไม่บังคับ occurrenceLimit (1–1000), postLeadDays (0–31), reminderLeadDays (0–30) ถ้าขาดฟิลด์ตารางหรือ ค่าอยู่นอกช่วงจะได้ 400 nextRun คำนวณจากตารางถ้าไม่ส่ง
PUT /v1/recurring/<id> merge nextRun คำนวณใหม่เมื่อมีฟิลด์ตาราง ใน body 400 ถ้าเป็นกฎผูกกู้
DELETE /v1/recurring/<id> 400 ถ้าเป็นกฎผูกกู้ 404 ถ้าไม่มี
GET /v1/invest/prices บัญชีลงทุนที่ยังถืออยู่ พร้อมราคาล่าสุดที่บันทึกไว้ของแต่ละตัว {accounts:[{id,name,currency,holdings:[{ticker,shares,price,priceUpdatedAt,stale}]}]} ใช้ได้เฉพาะแบบ Premium (ถ้าไม่ใช่ จะได้ 403 premium_required)
PUT /v1/invest/prices {accountId, prices:{TICKER:price,…}, asOf?} ตั้งราคาหลายตัวพร้อมกัน asOf คือวันที่ของราคา (YYYY-MM-DD ไม่ใช่อนาคต) ตัวที่ไม่ได้ถืออยู่หรือราคาไม่เป็นบวก → 400 ไม่บันทึกอะไรเลย บัญชีไม่มี → 404 เฉพาะแบบ Premium
GET /v1/server/fingerprint ลายนิ้วมือ SHA-256 ของใบรับรอง → {"sha256":"AB:CD:…"}
GET /v1/theme ชุดสีที่ผู้ใช้เลือกในแอปและสีของหมวดหมู่/แท็ก → {"flavor":"warm","categoryPalette":[{"bg":"#f5e7e3","fg":"#be4a42"},…]} (อ้างด้วย colorIndex)

tagIds — POST /v1/entries, PUT /v1/entries/<id> และทุก batch item รับ tagIds: [<int>…]: ไม่ส่ง = ไม่แตะแท็กเดิม, [] = ล้างแท็ก, ลิสต์ = แทนที่ทั้งชุด id ที่ไม่รู้จัก → 400

บัญชีชนิด loan ถูกปฏิเสธ (400) เมื่อใช้เป็นบัญชี income/expense หรือเป็นบัญชีต้นทางของ transfer — เป็นได้แค่ปลายทางของ transfer (การชำระคืน)

06

GET /v1/health

ตรวจว่าเซิร์ฟเวอร์ทำงาน คืน { "status": "ok" }

07

POST /v1/entries/batch

สร้างรายการทีเดียวได้หลายรายการ ถ้าจะเพิ่มแค่รายการเดียว POST /v1/entries (ดูรายการ route ทั้งหมด) ง่ายกว่า

{
  "autoCreate": false,
  "entries": [
    { "type": "expense", "amount": 60, "account": "SCB", "category": "อาหาร" },
    { "type": "transfer", "amount": 500, "account": "SCB", "toAccount": "Wallet" }
  ]
}

entries ต้องเป็น array ที่ไม่ว่าง สูงสุด 100 รายการ แต่ละรายการ:

ฟิลด์ บังคับ หมายเหตุ
type ใช่ income, expense หรือ transfer (ไม่สนตัวพิมพ์)
amount ใช่ ตัวเลข ≥ 0
account ใช่ id (ตัวเลข) หรือชื่อบัญชีที่มีอยู่ — เป็นบัญชี "ต้นทาง" ของ transfer
toAccount เฉพาะ transfer id หรือชื่อบัญชีปลายทาง ต้องต่างจาก account
category เฉพาะ income/expense ชื่อหมวดหมู่ของประเภทนั้น
title ไม่ ข้อความอิสระ ค่าเริ่มต้น ""
note ไม่ ข้อความอิสระ
timestamp ไม่ epoch milliseconds ค่าเริ่มต้นคือตอนนี้
idempotencyKey ไม่ ส่ง key เดิมซ้ำ → ได้รายการเดิมกลับ ("idempotent": true) ไม่สร้างใหม่ รวมถึงกรณีส่งซ้ำระหว่างที่คำขอแรกยังเขียนไม่เสร็จ ถ้ารายการนั้นถูกลบไปแล้ว การส่งซ้ำจะได้ 400 ให้ใช้ key ใหม่ เก็บใน หน่วยความจำเท่านั้น (สูงสุด 1000 key ล้างเมื่อรีสตาร์ท)
tagIds ไม่ array ของ id แท็ก (GET /v1/tags) — [] = ไม่มีแท็ก, id ที่ไม่รู้จัก → item ล้ม

การสร้างแบบ batch เป็นแบบ best-effort — ตรวจและสร้างแต่ละรายการแยกกัน รายการที่ผิดไม่ทำให้รายการอื่นล้ม เป็น loop ธรรมดา ไม่ใช่ database transaction

สร้างบัญชี/หมวดหมู่อัตโนมัติ

โดยปกติชื่อ account/toAccount หรือ category ที่ไม่มีอยู่จะเป็น error ของรายการนั้น ใส่ "autoCreate": true ที่ระดับบนสุด ของ request (มีผลทั้ง batch) เพื่อสร้างสิ่งที่ขาดแทน:

  • บัญชี/toAccount ที่ส่งมาเป็นชื่อ → สร้างบัญชีใหม่ชื่อนั้น ประเภทเป็นชนิดทรัพย์สินแรกของคุณ (ปกติคือเงินสด) ยอด 0 — ส่งมาเป็น id ไม่มีการสร้างอัตโนมัติ id ที่ไม่รู้จักถูกปฏิเสธเสมอ
  • หมวดหมู่ → สร้างหมวดหมู่ใหม่ตามประเภทของรายการ สีคำนวณจากชื่อ ไอคอนทั่วไป

บัญชี/หมวดหมู่ที่เพิ่งสร้าง รายการถัด ๆ ไปใน batch เดียวกันเห็นทันที

ผลลัพธ์

201 เมื่อทุกรายการถูกสร้างใหม่หมด 200 กรณีอื่น (บางรายการล้ม หรือเป็น idempotent replay):

{
  "created": 1,
  "idempotent": 0,
  "failed": 1,
  "results": [
    { "success": true, "entry": { "id": 107, "title": "", "amount": 60.0,
      "type": "EXPENSE", "category": "อาหาร", "account": 1, "toAccount": null,
      "note": null, "timestamp": 1786806400000, "currency": "THB", "tags": [] } },
    { "success": false, "error": "\"category\" must match an existing expense category" }
  ]
}

currency เป็นสกุลเงินหลักของแอปเสมอ (ตั้งค่า → "สกุลเงินหลัก") ไม่มีการตั้งต่อ request

บัญชีชนิด 'loan' ที่ใช้เป็นบัญชีรายรับ/รายจ่าย หรือ เป็นบัญชีต้นทางของการโอน จะล้มเป็นราย item ด้วย "A loan account can only be the destination of a transfer …" (บัญชีกู้ยืมเป็นได้แค่ปลายทางของการโอนเท่านั้น)

error ทั้ง request: 400 ถ้า entries หาย/ว่าง/ไม่ใช่ array, เกิน 100 รายการ หรือ body ไม่ใช่ JSON ที่ถูกต้อง; 413 ถ้า body เกิน 1 MiB

08

การอ่านข้อมูล

แค่อยากดูยอดคงเหลือเฉยๆ? ใช้วิดเจ็ตหน้าโฮม (หรือ tangla-manage) แทน

เอนด์พอยต์นี้เหมาะกับตอนดึงยอดไปใช้ต่อในที่อื่น — สคริปต์, วิดเจ็ตของแอปอื่น (KWGT/Zooper), ตัวแปรใน Tasker — ไม่ใช่ไว้ดูเฉยๆ ถ้าแค่อยากดูยอดบนมือถือเครื่องนี้เอง วิดเจ็ต Balance/Budget ของตังค์ละทำได้ดีกว่ามาก เพราะอ่านฐานข้อมูลตรง ในเครื่อง ไม่ผ่าน HTTP เลย จึงยังโชว์ยอดล่าสุดได้แม้จอดับหรือปิด สวิตช์ API ภายนอกไว้

อยากดูจากอุปกรณ์อื่นในวง LAN ก็เปิดหน้าเว็บ tangla-manage ตรงๆ แทนการเขียนสคริปต์ดึงเป็นระยะ — เพราะไม่ว่าจะ scope ไหน เซิร์ฟเวอร์ก็หยุดทำงานทันทีที่มือถือจอดับเหมือนกัน การเปิด "เปิดใช้งาน API" ค้างไว้เป็นแค่ค่าที่บันทึกไว้ ไม่ได้แปลว่าเรียกได้ตลอดเวลา

เปิดใช้งาน API + ตั้งชื่อผู้ใช้/รหัสผ่านแล้วเรียกได้เลย — login แล้วเอา JWT มา GET สองเอนด์พอยต์นี้ได้ทันที

GET /v1/summary

?cycle=<epoch-ms> เลือกรอบบิลที่ครอบเวลานั้น (ค่าเริ่มต้น: ตอนนี้) การจ่ายบัตรเครดิตนับเป็นรายจ่าย ตรงกับหน้าโฮม

{
  "cycle": { "key": "2026-09", "start": 1788220800000, "end": 1790812799999 },
  "income": 15000.0,
  "expense": 4230.5,
  "net": 10769.5,
  "netBalance": 88123.0
}

netBalance = ผลรวมยอดของบัญชีที่ "นับรวมในยอดรวม" ทุกบัญชี

GET /v1/accounts

ทุกบัญชีพร้อมยอดคงเหลือปัจจุบัน

[
  { "id": 1, "name": "เงินสด", "type": "cash", "balance": 1234.0,
    "currency": "THB", "includeInTotal": true, "includeInNetWorth": true,
    "isHidden": false, "note": null }
]
09

ตัวอย่าง curl

HOST="<ที่อยู่จากแอป เช่น https://192.168.1.117:53214>"
USERNAME="<ชื่อผู้ใช้ที่ตั้งไว้ในแอป>"
PASSWORD="<รหัสผ่านที่ตั้งไว้ในแอป>"

# login ครั้งเดียว ใช้โทเค็นซ้ำได้นาน 60 นาที (-k เพราะใบรับรองเป็น self-signed)
TOKEN=$(curl -sk -X POST "$HOST/v1/auth/login" \
  -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')

# ตรวจสถานะ
curl -k -H "Authorization: Bearer $TOKEN" "$HOST/v1/health"

# รายจ่ายหนึ่งรายการ อ้างบัญชีด้วยชื่อ
curl -k -X POST "$HOST/v1/entries/batch" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"entries":[{"type":"expense","amount":60,"account":"SCB","category":"อาหาร","title":"กาแฟ"}]}'

# โอนหนึ่งรายการ
curl -k -X POST "$HOST/v1/entries/batch" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"entries":[{"type":"transfer","amount":500,"account":"SCB","toAccount":"True Wallet"}]}'

# สร้างบัญชี/หมวดหมู่ถ้ายังไม่มี และกันยิงซ้ำด้วย key
curl -k -X POST "$HOST/v1/entries/batch" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"autoCreate":true,"entries":[
        {"type":"expense","amount":1000,"account":"กองทุนฉุกเฉิน",
         "category":"ลงทุน","idempotencyKey":"sep-invest-2026-09"}
      ]}'

# สรุปของรอบนี้
curl -k -H "Authorization: Bearer $TOKEN" "$HOST/v1/summary"
10

หาชื่อบัญชีและหมวดหมู่

ชื่อบัญชีและหมวดหมู่คือสิ่งที่แอปแสดงตรง ๆ (ตั้งค่า → "บัญชีทรัพย์สิน" และ "หมวดหมู่") ส่วน id (ตัวเลข) ก็ใช้ได้แต่ไม่แสดงใน UI — เรียก GET /v1/accounts / GET /v1/categories หรือส่ง batch หนึ่งรายการที่ใส่หมวดหมู่ผิดตั้งใจ โดยปิด autoCreate: ค่าใน results[0].error บอกว่า id ถูกต้องไหม ("No account with id <n>" เทียบกับ "category" must match …) และไม่มีอะไรถูกสร้าง

11

ควรรู้

  • เซิร์ฟเวอร์เข้าถึงได้เฉพาะตอนแอปเปิดอยู่ ไม่มีโหมดเบื้องหลัง
  • ทุกการสร้าง/แก้ไข/ลบ ผ่าน write core เดียวกับหน้าเพิ่มรายการปกติ ยอดเงินและผลข้างเคียงจึงถูกต้อง
  • บัญชีแยกสกุลเงินได้เดียวทั้งเล่ม ไม่มีสกุลเงินต่อรายการ
  • ดูเพิ่มที่ นโยบายความเป็นส่วนตัว ว่าเซิร์ฟเวอร์นี้เปิดเผยและไม่เปิดเผยอะไรบ้าง