Skip to content

SDK สำหรับ Go ​

github.com/EASYSLIP-CO-LTD/sdk-go ช่วยให้บริการ Go ของคุณตรวจสลิปโอนเงินของธนาคารไทยและสลิป TrueMoney Wallet กับธนาคารได้ เช็กได้ว่าเงินเข้าบัญชีของคุณ ยอดเงินถูกต้อง และไม่ใช่สลิปซ้ำ รวมถึงรับผลการตรวจเบื้องหลังผ่านเว็บฮุคที่มีลายเซ็นยืนยัน ใช้แค่ standard library ทุกเมธอดรับ context.Context และคืน error แบบมี type ที่ใช้กับ errors.As ได้

หน้านี้เป็นคู่มือฉบับเต็ม ส่วน README บน GitHub เป็นฉบับสรุป และดูซอร์สโค้ดได้ที่ EASYSLIP-CO-LTD/sdk-go

สิ่งที่ต้องมี ​

การติดตั้ง ​

sh
go get github.com/EASYSLIP-CO-LTD/sdk-go

ชื่อ package คือ easyslip ตัวอย่างในหน้านี้ import เป็น easyslip "github.com/EASYSLIP-CO-LTD/sdk-go" เพื่อให้เห็นชื่อชัดเจน

เริ่มต้นใช้งาน ​

โปรแกรมนี้ตรวจสลิปที่ลูกค้าส่งมาจ่ายออเดอร์ยอด 1,500 บาท โดยเช็กว่าเงินเข้าบัญชีรับเงินที่ลงทะเบียนไว้ ยอดเงินถูกต้อง และสลิปไม่ซ้ำ จากนั้นพิมพ์รายละเอียดการโอนออกมา

บันทึกโค้ดเป็นไฟล์ main.go ใน module ใหม่ แล้ววางรูปสลิปไว้ข้างกันในชื่อ slip.jpg

go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("") // อ่าน API key จาก EASYSLIP_API_KEY
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	orderTotal := 1500.0 // บาท
	result, err := client.Verify.BankByImage(context.Background(), "slip.jpg", &easyslip.VerifyOptions{
		MatchAccount:   easyslip.Some(true),       // เงินเข้าบัญชีรับเงินของเราไหม
		MatchAmount:    easyslip.Some(orderTotal), // ยอดเงินถูกไหม
		CheckDuplicate: easyslip.Some(true),       // เป็นสลิปซ้ำไหม
	})
	var notFound *easyslip.SlipNotFoundError
	if errors.As(err, &notFound) {
		fmt.Println("ไม่พบรายการโอนนี้ที่ธนาคาร สลิปอาจเป็นของปลอมหรือถูกแก้ไข")
		return
	}
	if err != nil {
		log.Fatal(err)
	}

	duplicate, checked := result.IsDuplicate.Value()
	amountMatched, _ := result.IsAmountMatched.Value()
	account, _ := result.MatchedAccount.Value()
	slip := result.RawSlip
	switch {
	case !checked || duplicate:
		fmt.Println("สลิปนี้เคยใช้แล้ว อย่ายืนยันออเดอร์ซ้ำ")
	case !amountMatched:
		fmt.Printf("สลิปนี้โอน %v บาท แต่ออเดอร์ยอด %v บาท\n", result.AmountInSlip, orderTotal)
	case account == nil:
		fmt.Println("สลิปนี้ไม่ได้โอนเข้าบัญชีรับเงินของเรา")
	default:
		sender, _ := slip.Sender.Account.Name.Th.Value()
		receiver, _ := slip.Receiver.Account.Name.Th.Value()
		fmt.Println("ยืนยันการชำระเงินแล้ว")
		fmt.Printf("ยอดเงิน:   %v บาท\n", slip.Amount.Amount)
		fmt.Printf("วันที่:     %s\n", slip.Date)
		fmt.Printf("ผู้โอน:    %s\n", sender)
		fmt.Printf("ผู้รับ:     %s\n", receiver)
		fmt.Printf("เลขอ้างอิง: %s\n", slip.TransRef)
	}
}

รันพร้อม API key ของคุณ

sh
EASYSLIP_API_KEY="your-api-key" go run .

ถ้าสลิปถูกต้อง จะได้ผลแบบนี้

text
ยืนยันการชำระเงินแล้ว
ยอดเงิน:   1500 บาท
วันที่:     2024-01-15T14:30:00+07:00
ผู้โอน:    นาย ผู้โอน ทดสอบ
ผู้รับ:     บริษัท ตัวอย่าง จำกัด
เลขอ้างอิง: 68370160657749I376388B35

MatchAccount เทียบบัญชีผู้รับกับบัญชีรับเงินที่คุณลงทะเบียนไว้ จึงต้องเพิ่มบัญชีรับเงินก่อน ทำได้ใน developer portal หรือใช้ BankAccounts.Create การตรวจสลิปแต่ละครั้งใช้โควต้า 1 ครั้ง

โปรแกรมนี้รับยอดเฉพาะเมื่อแต่ละเงื่อนไขได้ค่าที่หมายถึงผ่านจริง Value() คืนค่าที่สองเป็น false เมื่อ API ไม่ได้ส่งผลของเงื่อนไขนั้นมา และโปรแกรมถือว่าไม่ผ่านเช่นกัน ดูเรื่อง Field ได้ที่ฟิลด์ที่อาจไม่มีค่า

ยอดของออเดอร์ต้องดึงจากฐานข้อมูลของคุณเอง ห้ามรับค่าจากลูกค้า ยืนยันออเดอร์เฉพาะเมื่อผ่านครบทั้งสามเงื่อนไข และเก็บ RawSlip.TransRef ในคอลัมน์ที่ตั้ง unique constraint ไว้ ภายใน transaction เดียวกับที่บันทึกว่าออเดอร์จ่ายแล้ว เพื่อไม่ให้คำขอสองรายการที่ใช้สลิปใบเดียวกันยืนยันออเดอร์ได้ทั้งคู่

อ่านต่อ

สร้าง client ​

สร้าง client ครั้งเดียวตอนบริการเริ่มทำงาน ใช้ร่วมกันได้ทุก goroutine แล้วเรียก Close ตอนบริการหยุด NewClient รับ API key หรือ "" เพื่ออ่านจาก environment ตามด้วย option ค่าที่ไม่ได้ตั้งจะอ่านจากตัวแปร environment ถ้าไม่มีก็ใช้ค่าเริ่มต้น

optionตัวแปร environmentค่าเริ่มต้นใช้ทำอะไร
NewClient(apiKey)EASYSLIP_API_KEYไม่มีAPI key ของคุณ ถ้าไม่มี key เลย NewClient จะคืน *InputError
ไม่มี optionEASYSLIP_API_KEY_FILEไม่มีpath ของไฟล์ที่เก็บ API key เช่น Docker secret หรือ Kubernetes secret ใช้เมื่อส่ง key เป็น "" และไม่มี EASYSLIP_API_KEY
WithTimeout(d)ไม่มีตัวแปร30 * time.Secondเวลาสูงสุดของการส่งคำขอแต่ละครั้ง
WithMaxRetries(n)ไม่มีตัวแปร2จำนวนครั้งที่ลองใหม่เมื่อคำขอล้มเหลว ตั้งได้ 0 ถึง 20 ใส่ 0 เพื่อปิด
WithHTTPClient(c)ไม่มีตัวแปรhttp.Client ตัวใหม่*http.Client ที่ SDK ใช้ส่งคำขอ
WithBaseURLs(v2, v1)EASYSLIP_V2_BASE_URL, EASYSLIP_V1_BASE_URLhttps://api.easyslip.com/v2, https://api.easyslip.com/v1ที่อยู่ของ API v2 และ API v1
go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("",
		easyslip.WithTimeout(15*time.Second), // ให้เวลาแต่ละครั้ง 15 วินาที
		easyslip.WithMaxRetries(1),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	info, err := client.GetInfo(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(info.Application.Name)
}

easyslip.EasySlip เป็นอีกชื่อหนึ่งของ easyslip.Client

ใช้ HTTP client ของคุณเอง ​

ส่ง *http.Client ของคุณเข้าไปได้ เช่น เพื่อส่งคำขอผ่าน proxy ครอบ transport ไว้เก็บ log หรือ tracing หรือบันทึกคำขอในเทสต์ SDK ใช้สำเนาของ client นั้น ใช้ WithTimeout กับการส่งแต่ละครั้ง และไม่ follow redirect เลย Close จะไม่ยุ่งกับ client ที่คุณส่งเข้ามา คุณจึงต้องปิด idle connection เอง

go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

type loggingTransport struct{ next http.RoundTripper }

func (t loggingTransport) RoundTrip(request *http.Request) (*http.Response, error) {
	started := time.Now()
	response, err := t.next.RoundTrip(request)
	if err == nil {
		log.Printf("%s %s %d %s", request.Method, request.URL, response.StatusCode, time.Since(started))
	}
	return response, err
}

func main() {
	httpClient := &http.Client{Transport: loggingTransport{next: http.DefaultTransport.(*http.Transport).Clone()}}
	defer httpClient.CloseIdleConnections()

	client, err := easyslip.NewClient("", easyslip.WithHTTPClient(httpClient))
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	info, err := client.GetInfo(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(info.Application.Name)
}

client เริ่มต้นของ SDK ไม่อ่านการตั้งค่า proxy จาก environment ถ้าต้องการใช้ proxy ให้ตั้ง Proxy ใน transport ของ client ที่ส่งเข้าไป

ตรวจสลิปธนาคาร ​

เลือกเมธอดตามข้อมูลที่ลูกค้าส่งมา ทั้งสี่เมธอดเรียก POST /verify/bank คืนผลการตรวจแบบเดียวกัน และรับตัวเลือกชุดเดียวกัน ถ้าไม่ต้องการตรวจเงื่อนไขใดเลยให้ส่ง nil

ข้อมูลที่มีเมธอดเอกสาร API
ไฟล์รูป ไฟล์ที่อัปโหลดมา หรือ streamVerify.BankByImage(ctx, image, options)อัปโหลดรูป
ข้อความในคิวอาร์โค้ดของสลิปที่อ่านด้วยตัวสแกนVerify.BankByPayload(ctx, payload, options)Payload
URL แบบ https ของรูปสลิปที่เปิดได้จากภายนอกVerify.BankByURL(ctx, url, options)URL
รูปที่เข้ารหัสเป็น base64 จะมี data: นำหน้าหรือไม่ก็ได้Verify.BankByBase64(ctx, base64, options)Base64
go
package main

import (
	"context"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	result, err := client.Verify.BankByURL(context.Background(), "https://example.com/slips/1001.jpg", nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.RawSlip.TransRef, result.AmountInSlip)
}

payload คือข้อความที่เข้ารหัสอยู่ในคิวอาร์โค้ดของสลิป ไม่ใช่เลขอ้างอิงที่พิมพ์บนสลิป ถ้าแอปของคุณสแกนคิวอาร์เองได้ BankByPayload จะเร็วที่สุดเพราะไม่ต้องอัปโหลดอะไร

รูปแบบรูปที่รับได้ ​

BankByImage และ TruewalletByImage รับข้อมูลได้ดังนี้

  • path ของไฟล์ในรูปแบบ string
  • รูปที่เป็น []byte
  • io.Reader เช่น multipart.File ที่ได้จาก r.FormFile
  • easyslip.Image ที่สร้างจาก easyslip.OpenImage, easyslip.ReadImage หรือ easyslip.ImageBytes

รูปต้องเป็น JPEG, PNG, GIF หรือ WebP ขนาดไม่เกิน 4 MB SDK ตรวจ bytes ของไฟล์ก่อนอัปโหลด และจะคืน *InputError ถ้าไฟล์ว่าง ใหญ่เกิน 4 MB หรือไม่ใช่รูป

string คือ path ของไฟล์

string ที่ส่งเข้า BankByImage จะถูกอ่านเป็น path ของไฟล์บนเซิร์ฟเวอร์เสมอ ไฟล์ที่ลูกค้าอัปโหลดต้องส่งเป็น []byte หรือ io.Reader ห้ามส่งเป็น string ที่ลูกค้ากำหนดค่าได้

handler นี้รับสลิปที่อัปโหลดมาทางฟอร์มในช่องชื่อ slip

go
package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"time"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func uploadSlip(client *easyslip.Client) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		r.Body = http.MaxBytesReader(w, r.Body, 5<<20)
		file, _, err := r.FormFile("slip")
		if err != nil {
			http.Error(w, `กรุณาแนบรูปสลิปในช่อง "slip"`, http.StatusBadRequest)
			return
		}
		defer file.Close()

		result, err := client.Verify.BankByImage(r.Context(), file, &easyslip.VerifyOptions{CheckDuplicate: easyslip.Some(true)})
		var badImage *easyslip.InvalidImageError
		if errors.As(err, &badImage) {
			http.Error(w, "กรุณาอัปโหลดรูปสลิปที่ชัดเจน", http.StatusUnprocessableEntity)
			return
		}
		if err != nil {
			http.Error(w, "ตรวจสลิปไม่สำเร็จ", http.StatusBadGateway)
			return
		}
		fmt.Fprintf(w, "เลขอ้างอิง %s ยอด %v บาท", result.RawSlip.TransRef, result.AmountInSlip)
	}
}

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	mux := http.NewServeMux()
	mux.Handle("POST /slips", uploadSlip(client))
	server := &http.Server{Addr: ":8000", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
	log.Fatal(server.ListenAndServe())
}

เช็กยอดเงิน บัญชีรับเงิน และสลิปซ้ำ ​

เปิดการตรวจที่ต้องการด้วยฟิลด์ของ VerifyOptions โดยครอบค่าแต่ละตัวด้วย easyslip.Some ตัวเลือกแต่ละตัวจะเพิ่มฟิลด์ในผลการตรวจ ถ้าไม่ใส่ก็จะไม่มีฟิลด์นั้น เมื่อเงื่อนไขไม่ผ่าน API ยังคืนข้อมูลสลิปมาให้ตามปกติ โค้ดของคุณเป็นคนตัดสินว่าจะรับยอดหรือไม่

ตัวเลือกตรวจอะไรฟิลด์ในผลการตรวจเมื่อไม่ผ่าน
MatchAmount: easyslip.Some(1500.0)ยอดเงินในสลิปตรงกับยอดที่คาดไว้ หน่วยเป็นบาทAmountInOrder, IsAmountMatchedIsAmountMatched เป็น false
MatchAccount: easyslip.Some(true)ผู้รับเงินเป็นบัญชีรับเงินที่ลงทะเบียนไว้MatchedAccountMatchedAccount เป็น null
CheckDuplicate: easyslip.Some(true)สลิปนี้เคยถูกตรวจมาแล้วIsDuplicateIsDuplicate เป็น true
Remark: easyslip.Some("Order 1001")ไม่ได้ตรวจอะไร แค่บันทึกโน้ตยาว 1 ถึง 255 ตัวอักษรไว้กับการตรวจครั้งนี้Remarkไม่มีกรณีไม่ผ่าน
go
package main

import (
	"context"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	result, err := client.Verify.BankByImage(context.Background(), "slip.jpg", &easyslip.VerifyOptions{
		Remark:         easyslip.Some("Order 1001"),
		MatchAccount:   easyslip.Some(true),
		MatchAmount:    easyslip.Some(1500.0),
		CheckDuplicate: easyslip.Some(true),
	})
	if err != nil {
		log.Fatal(err)
	}
	amountMatched, _ := result.IsAmountMatched.Value()
	account, _ := result.MatchedAccount.Value()
	duplicate, checked := result.IsDuplicate.Value()
	accepted := amountMatched && account != nil && checked && !duplicate
	remark, _ := result.Remark.Value()
	fmt.Println(map[bool]string{true: "รับยอด", false: "ไม่รับยอด"}[accepted], remark)
}

ถ้าสาขาเดียวกันเคยตรวจสลิปนี้แล้ว API จะคืนผลเดิมที่บันทึกไว้และไม่ใช้โควต้า ถ้าเป็นสาขาอื่นที่เคยตรวจ API จะตรวจกับธนาคารใหม่และใช้โควต้า ทั้งสองกรณี IsDuplicate เป็น true

MatchAccount เทียบเฉพาะบัญชีที่ผูกกับสาขาของ API key ถ้าสลิปที่ควรผ่านกลับได้ MatchedAccount เป็น null ให้ตรวจว่าบัญชีรับเงินนั้นผูกกับสาขานี้แล้ว

ฟิลด์ที่อาจไม่มีค่า ​

ฟิลด์ที่ API อาจไม่ส่งมา หรือส่งมาเป็น JSON null มีชนิดเป็น easyslip.Field[T]

  • Value() คืนค่ากับ true เมื่อฟิลด์มีค่า และคืน zero value กับ false เมื่อไม่มีฟิลด์นั้นหรือเป็น null
  • IsSet() บอกว่า API ส่งฟิลด์นี้มาหรือไม่ และ IsNull() บอกว่าส่งมาเป็น null หรือไม่
  • ในตัวเลือก ใช้ easyslip.Some(v) เพื่อส่งค่า และ easyslip.Null[T]() เพื่อส่ง null ฟิลด์ที่ปล่อยเป็น zero value จะไม่ถูกส่ง

เช่น MatchedAccount จะไม่มีค่าเมื่อไม่ได้ใส่ MatchAccount และจะมีแต่เป็น null เมื่อใส่แล้วแต่สลิปไม่ได้โอนเข้าบัญชีไหนของคุณเลย

สลิป TrueMoney Wallet ​

สลิป TrueMoney Wallet ใช้งานแบบเดียวกันและรับตัวเลือกชุดเดียวกัน เมธอดกลุ่มนี้เรียก POST /verify/truewallet และข้อมูลสลิปในผลการตรวจสั้นกว่า

go
package main

import (
	"context"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	result, err := client.Verify.TruewalletByImage(context.Background(), "wallet-receipt.png", &easyslip.VerifyOptions{
		MatchAmount:    easyslip.Some(500.0),
		CheckDuplicate: easyslip.Some(true),
	})
	if err != nil {
		log.Fatal(err)
	}
	slip := result.RawSlip
	fmt.Println(slip.TransactionID, slip.Amount, slip.Receiver.Name)
}

ใช้ Verify.TruewalletByURL เมื่อมี URL ของรูป และ Verify.TruewalletByBase64 เมื่อมี base64 สลิป TrueMoney Wallet ไม่มีเมธอดแบบ payload

ข้อมูลบัญชีและโควต้าคงเหลือ ​

GetInfo เรียก GET /info แล้วคืนข้อมูลแอปพลิเคชัน สาขา และโควต้า ถ้าแพ็กเกจไม่จำกัดโควต้า Quota.Remaining จะเป็น nil

go
package main

import (
	"context"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	info, err := client.GetInfo(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	quota := info.Application.Quota
	remaining := "ไม่จำกัด"
	if quota.Remaining != nil {
		remaining = fmt.Sprint(*quota.Remaining)
	}
	fmt.Printf("%s, สาขา %s\n", info.Application.Name, info.Branch.Name)
	fmt.Printf("ใช้ไป %d คงเหลือ %s\n", quota.Used, remaining)
}

บัญชีรับเงิน ​

MatchAccount เทียบผู้รับเงินในสลิปกับบัญชีกลุ่มนี้ บัญชีรับเงินเป็นของบริการ (service) และแต่ละสาขาจะเทียบเฉพาะบัญชีที่ผูกกับสาขานั้น BankAccounts.Create เพิ่มบัญชีแล้วผูกกับสาขาของ API key ให้ทันที ดูรายละเอียดแต่ละ endpoint ได้ที่บัญชีธนาคาร

go
package main

import (
	"context"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	account, err := client.BankAccounts.Create(ctx, easyslip.CreateBankAccountParams{
		BankCode:   "004", // ดูรหัสธนาคารได้จาก client.Banks.List
		BankNumber: "1234567890",
		NameTh:     "บริษัท ตัวอย่าง จำกัด",
		NameEn:     "EXAMPLE CO., LTD.",
		Type:       easyslip.AccountTypeJuristic,
	})
	if err != nil {
		log.Fatal(err)
	}
	if _, err := client.BankAccounts.Update(ctx, account.ID, easyslip.UpdateBankAccountParams{
		NameEn: easyslip.Some("EXAMPLE COMPANY LIMITED"),
	}); err != nil {
		log.Fatal(err)
	}

	// บัญชีที่ผูกกับสาขานี้ทีละหน้า
	page, err := client.BankAccounts.List(ctx, &easyslip.ListBankAccountsParams{Page: easyslip.Some(1), Limit: easyslip.Some(20)})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("ผูกกับสาขานี้ %d บัญชี\n", page.Total)

	// ทุกบัญชีในบริการ SDK ดึงทุกหน้าให้เอง
	for item, err := range client.BankAccounts.IterateAll(ctx, nil) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(item.ID, item.BankNumber, item.NameEn)
	}
}
เมธอดใช้ทำอะไร
BankAccounts.Create(ctx, params)เพิ่มบัญชีเข้าบริการและผูกกับสาขานี้
BankAccounts.Get(ctx, id)ดึงข้อมูลบัญชีเดียว
BankAccounts.Update(ctx, id, params)แก้เฉพาะฟิลด์ที่ตั้งค่าไว้ ใส่ ExtraVerify: easyslip.Null[*string]() เพื่อล้างค่านั้น
BankAccounts.Link(ctx, id)ผูกบัญชีของบริการกับสาขานี้ ผูกซ้ำก็ไม่เป็นไร
BankAccounts.Unlink(ctx, id)ยกเลิกการผูกกับสาขานี้ บัญชียังอยู่ในบริการ
BankAccounts.List(ctx, params)ดึงบัญชีที่ผูกกับสาขานี้หนึ่งหน้า Page เริ่มที่ 1 และ Limit อยู่ระหว่าง 1 ถึง 100 (ค่าเริ่มต้น 20)
BankAccounts.ListAll(ctx, params)ดึงบัญชีทั้งหมดของบริการหนึ่งหน้า แบ่งหน้าแบบเดียวกัน
BankAccounts.Iterate(ctx, params)ไล่ดูทุกบัญชีที่ผูกกับสาขานี้ ดึงหน้าละ 100 บัญชี คืนเป็น iter.Seq2[BankAccount, error]
BankAccounts.IterateAll(ctx, params)ไล่ดูทุกบัญชีของบริการ ดึงหน้าละ 100 บัญชี

Type เป็น easyslip.AccountType ใช้ easyslip.AccountTypeNatural ("NATURAL") สำหรับบัญชีบุคคลธรรมดา หรือ easyslip.AccountTypeJuristic ("JURISTIC") สำหรับบัญชีนิติบุคคล ใน UpdateBankAccountParams และ ListBankAccountsParams ให้ใส่เป็น easyslip.Some(easyslip.AccountTypeJuristic) เมธอดดึงรายการทั้งสี่ตัวรับ *ListBankAccountsParams ที่กรองด้วย Type และ BankCode ได้ หรือส่ง nil ก็ได้ ถ้าเพิ่มบัญชีที่มีอยู่แล้วจะได้ *ConflictError

รหัสธนาคาร ​

Banks.List เรียก GET /banks แล้วคืนรายชื่อธนาคารทั้งหมดที่ API รู้จัก พร้อม Code ที่ใช้เป็น BankCode รายการเดียวกันนี้ดูได้ที่รหัสธนาคาร

go
package main

import (
	"context"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	banks, err := client.Banks.List(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	for _, bank := range banks.Items {
		fmt.Println(bank.Code, bank.ShortCode, bank.NameTh)
	}
}

ตรวจสลิปจำนวนมากเบื้องหลัง ​

งานเบื้องหลัง (job) จะตรวจสลิปที่ฝั่ง EasySlip แล้วส่งผลไปที่เว็บฮุคของคุณ เหมาะเมื่อมีสลิปจำนวนมากหรือไม่อยากรอธนาคารตอบ ต้องส่ง CallbackURL แบบ https ทุกครั้ง เว้นแต่สาขาตั้ง Default Webhook URL ไว้แล้ว ตรวจเบื้องหลังได้เฉพาะสลิปธนาคาร ดูรายละเอียด endpoint ได้ที่ Async Verify

ตัวอย่างนี้อ่านข้อความคิวอาร์ของสลิปหลายใบจาก payloads.txt บรรทัดละใบ แล้วส่งตรวจเป็นชุดเดียว

go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	text, err := os.ReadFile("payloads.txt")
	if err != nil {
		log.Fatal(err)
	}
	var slips []easyslip.BatchSlip
	for _, payload := range strings.Fields(string(text)) {
		slips = append(slips, easyslip.BatchSlip{Payload: payload, CheckDuplicate: easyslip.Some(true)})
	}

	batch, err := client.Jobs.SubmitBankBatch(ctx, slips, &easyslip.BatchOptions{
		CallbackURL: easyslip.Some("https://example.com/webhooks/easyslip"),
	})
	if err != nil {
		log.Fatal(err)
	}

	// เว็บฮุคจะได้รับผลทุกใบ หรือจะรอผลของแต่ละงานตรงนี้ก็ได้
	for _, submitted := range batch.Jobs {
		job, err := client.Jobs.WaitForJob(ctx, submitted.JobID, nil)
		if err != nil {
			log.Fatal(err)
		}
		if result, ok := job.Result.Value(); ok && result.Data != nil {
			fmt.Println(job.JobID, job.Status, result.Status, result.Data.AmountInSlip)
		} else {
			fmt.Println(job.JobID, job.Status)
		}
	}
}
เมธอดใช้ทำอะไร
Jobs.SubmitBank(ctx, payload, options)ส่งสลิปหนึ่งใบ AsyncVerifyOptions รับตัวเลือกเหมือน VerifyOptions และเพิ่ม CallbackURL
Jobs.SubmitBankBatch(ctx, slips, options)ส่งสลิป 1 ถึง 100 ใบในคำขอเดียว BatchSlip แต่ละใบตั้งตัวเลือกของตัวเองได้ และ BatchOptions เก็บ CallbackURL
Jobs.SubmitBankMany(ctx, slips, options)ส่งสลิปกี่ใบก็ได้ โดยแบ่งเป็นชุดละ 100 ใบแล้วส่งทีละชุด และคืนรายการของชุดที่ส่ง
Jobs.Get(ctx, jobID)ดึงสถานะงาน และผลการตรวจเมื่องานเสร็จ
Jobs.WaitForJob(ctx, jobID, options)เช็กงานทุก Interval (ค่าเริ่มต้น 2 วินาที) จนงานเป็น done หรือ failed รอนานสุด Timeout (ค่าเริ่มต้น 10 นาที) ส่ง *WaitOptions เพื่อเปลี่ยนค่าเหล่านี้ หรือส่ง nil

สลิปแต่ละใบในงานเบื้องหลังใช้โควต้าตอนที่ถูกตรวจ เหมือนการตรวจโดยตรง การเช็กงานด้วย Jobs.Get หรือ WaitForJob ไม่ใช้โควต้าและไม่ส่งสลิปไปตรวจซ้ำ API เก็บงานไว้ประมาณ 7 วัน ถ้าเว็บฮุคพลาดผลไป ก็ยังอ่านผลได้ด้วย Jobs.Get

งานจะผ่านสถานะ queued และ retrying ไปจนเป็น done หรือ failed สถานะ done แปลว่าธนาคารตอบแล้ว โดย Result.Status เป็น success พร้อมผลการตรวจใน Result.Data หรือเป็น not_found เมื่อลองครบแล้วธนาคารก็ยังไม่มีรายการโอนที่ตรงกัน ส่วน failed แปลว่าตรวจไม่สำเร็จ และ Error.Code บอกสาเหตุ เช่น QUOTA_EXCEEDED

ถ้า SubmitBankMany หยุดกลางทาง จะคืน *BatchSubmitError ชุดที่อยู่ใน Submitted ถูกรับไปแล้วและยังทำงานต่อ ให้ส่งใหม่เฉพาะสลิปตั้งแต่ชุด FailedChunk เป็นต้นไป

สร้างคิวอาร์รับเงิน ​

V1.GenerateQR สร้างคิวอาร์ PromptPay และคิวอาร์ชำระบิลให้ลูกค้าสแกนจ่ายได้ การสร้างคิวอาร์ไม่ใช้โควต้า Amount มีหน่วยเป็นบาท ใส่ได้ตั้งแต่ 0.01 ถึง 9,999,999,999.99 ทศนิยมไม่เกิน 2 ตำแหน่ง ถ้าไม่ใส่ ผู้จ่ายจะกรอกยอดเอง

go
package main

import (
	"context"
	"log"
	"os"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	// PromptPay เข้าเบอร์โทร ยอด 250 บาท
	promptPay, err := client.V1.GenerateQR(ctx, easyslip.QRRequest{
		Type:   easyslip.QRTypePromptPay,
		Msisdn: easyslip.Some("0812345678"),
		Amount: easyslip.Some(250.0),
	})
	if err != nil {
		log.Fatal(err)
	}
	save("promptpay.png", promptPay)

	// ชำระบิลเข้า Biller ID ของคุณ ใช้เลขใบแจ้งหนี้เป็น Ref1
	bill, err := client.V1.GenerateQR(ctx, easyslip.QRRequest{
		Type:     easyslip.QRTypePromptPay,
		BillerID: easyslip.Some("010555512345601"),
		Ref1:     easyslip.Some("INV1001"),
		Amount:   easyslip.Some(1500.0),
	})
	if err != nil {
		log.Fatal(err)
	}
	save("bill.png", bill)
}

func save(name string, qr *easyslip.QRCode) {
	image, err := qr.ImageBytes()
	if err != nil {
		log.Fatal(err)
	}
	if err := os.WriteFile(name, image, 0o644); err != nil {
		log.Fatal(err)
	}
}
Typeฟิลด์ที่ต้องใส่ฟิลด์ที่ใส่หรือไม่ก็ได้
QRTypePromptPayอย่างใดอย่างหนึ่ง: Msisdn (เบอร์โทร), NatID (เลขบัตรประชาชนหรือเลขผู้เสียภาษี), EWalletID หรือ BillerID ถ้าใช้ BillerID ต้องมี Ref1 ด้วยAmount และใส่ Ref2 ได้เมื่อใช้ BillerID
QRTypeKShopRef1 คือรหัสร้าน ขึ้นต้นด้วย KB ตามด้วยตัวเลข 12 หลักAmount
QRTypeMaeManeeRef1 คือรหัสร้าน 15 หลักAmount
QRTypeTungngernRef1 คือรหัสร้านค้า และ MerchantName คือชื่อร้านภาษาอังกฤษAmount

Type รับค่าคงที่ easyslip.QRType ในตาราง ส่วน easyslip.QRTypeBillPayment ("BILL_PAYMENT") แบบเดิมยังใช้ได้ ถ้าใช้ BillerID เดียวกันจะได้คิวอาร์เดียวกับ easyslip.QRTypePromptPay ผลลัพธ์มี Payload ซึ่งเป็นข้อความในคิวอาร์ด้วย ถ้าต้องการวาดคิวอาร์เอง

API v1 แบบเดิม ​

ใช้ client.V1 เฉพาะระบบที่เชื่อมกับ API v1 อยู่แล้ว โค้ดใหม่ควรใช้เมธอด v2 ด้านบน เมธอดของ v1 คืนข้อมูลสลิปโดยตรง และเมื่อใส่ CheckDuplicate แล้วเจอสลิปซ้ำจะคืน *DuplicateSlipError

go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	slip, err := client.V1.VerifyBankByImage(context.Background(), "slip.jpg", &easyslip.V1VerifyOptions{
		CheckDuplicate: easyslip.Some(true),
	})
	var duplicate *easyslip.DuplicateSlipError
	if errors.As(err, &duplicate) {
		if first, ok := duplicate.Slip.(*easyslip.BankSlip); ok {
			fmt.Println("สลิปนี้เคยใช้แล้ว วันที่โอน:", first.Date)
		}
		return
	}
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(slip.TransRef, slip.Amount.Amount)
}

API v1 ยังมี VerifyBankByPayload, VerifyBankByURL, VerifyBankByBase64, VerifyTruemoneyByImage และ GetMe ดูสิ่งที่เปลี่ยนใน v2 ได้ที่เปรียบเทียบเวอร์ชัน

Context และ goroutine ​

ทุกเมธอดรับ context.Context ยกเลิก context หรือกำหนด deadline ให้ เพื่อหยุดการเรียกและการลองใหม่ที่ตามมา การเรียกที่ถูกยกเลิกจะคืน error ที่ตรงกับ context.Canceled หรือ context.DeadlineExceeded เมื่อเช็กด้วย errors.Is

client ตัวเดียวใช้จากหลาย goroutine พร้อมกันได้อย่างปลอดภัย อย่ายิงคำขอพร้อมกันมากเกินไป เพราะ API จำกัดจำนวนคำขอต่อช่วงเวลา ตัวอย่างนี้ตรวจสลิปพร้อมกันได้ไม่เกินสามใบ

go
package main

import (
	"context"
	"fmt"
	"log"
	"sync"
	"time"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()

	files := []string{"slip-1001.jpg", "slip-1002.jpg", "slip-1003.jpg"}
	limit := make(chan struct{}, 3)
	var wg sync.WaitGroup
	for _, file := range files {
		wg.Go(func() {
			limit <- struct{}{}
			defer func() { <-limit }()
			result, err := client.Verify.BankByImage(ctx, file, &easyslip.VerifyOptions{CheckDuplicate: easyslip.Some(true)})
			if err != nil {
				fmt.Println(file, "ไม่สำเร็จ:", err)
				return
			}
			fmt.Println(file, result.RawSlip.TransRef)
		})
	}
	wg.Wait()
}

ถ้ามีสลิปมากกว่าไม่กี่ใบ ให้ส่งเป็นงานเบื้องหลังแทน

ผลการตรวจ ​

การตรวจแบบ v2 คืนผลของเงื่อนไขที่คุณขอไว้ที่ชั้นบนสุด และข้อมูลสลิปตามที่ธนาคารส่งมาไว้ใน RawSlip ดูโครงสร้างเต็มได้ที่เอกสาร API

สลิปธนาคาร ​

Verify.BankBy... คืน *BankSlipResult

ฟิลด์ชนิดความหมายตัวอย่าง
AmountInSlipfloat64ยอดเงินที่โอน หน่วยเป็นบาท1500
IsAmountMatchedField[bool]ยอดเงินตรงกับ MatchAmount หรือไม่ มีเฉพาะเมื่อใส่ MatchAmounttrue
AmountInOrderField[float64]ค่า MatchAmount ที่ส่งไป มีเฉพาะเมื่อใส่ MatchAmount1500
MatchedAccountField[*MatchedAccount]บัญชีรับเงินของคุณที่สลิปโอนเข้า หรือ null มีเฉพาะเมื่อใส่ MatchAccountBankNumber เป็น "123-4-56789-0"
IsDuplicateField[bool]สลิปนี้เคยถูกตรวจแล้วหรือไม่ มีเฉพาะเมื่อใส่ CheckDuplicatefalse
RemarkField[string]ค่า Remark ที่ส่งไป มีเฉพาะเมื่อส่ง"Order 1001"
RawSlip.TransRefstringเลขอ้างอิงรายการของธนาคาร แต่ละรายการโอนไม่ซ้ำกัน เก็บไว้เพื่อกันสลิปซ้ำได้"68370160657749I376388B35"
RawSlip.Datestringเวลาที่โอน รูปแบบ ISO 8601 พร้อมเขตเวลาไทย"2024-01-15T14:30:00+07:00"
RawSlip.Amount.Amountfloat64ยอดเงิน หน่วยเป็นบาท1500
RawSlip.FeeField[float64]ค่าธรรมเนียม เมื่อธนาคารแจ้งมา0
RawSlip.Sender.BankField[SlipBank]ธนาคารผู้โอน: ID (รหัสธนาคาร), Name และ Short"004", "กสิกรไทย", "KBANK"
RawSlip.Sender.Account.NameAccountNameชื่อผู้โอนภาษาไทย (Th) และอังกฤษ (En) เมื่อธนาคารแสดง"นาย ผู้โอน ทดสอบ", "MR. SENDER TEST"
RawSlip.Sender.Account.BankField[AccountNumber]เลขบัญชีผู้โอน (Account) และ Type ธนาคารปิดบางหลักไว้"BANKAC", "123-4-xxxxx-5"
RawSlip.Receiver.BankField[SlipReceiverBank]ธนาคารผู้รับ รูปแบบเดียวกับผู้โอน"014", "ไทยพาณิชย์", "SCB"
RawSlip.Receiver.Account.NameSlipReceiverAccountNameชื่อผู้รับ"บริษัท ตัวอย่าง จำกัด"
RawSlip.Receiver.Account.BankField[AccountNumber]เลขบัญชีผู้รับ ปิดบางหลักไว้"BANKAC", "xxx-x-x5678-x"
RawSlip.Receiver.Account.ProxyField[AccountNumber]PromptPay ที่ใช้รับเงิน เมื่อโอนผ่าน PromptPay"MSISDN", "08xxxxxxxx89"
RawSlip.Ref1, Ref2, Ref3Field[string]เลขอ้างอิงของการชำระบิล เป็นค่าว่างสำหรับการโอนทั่วไป""

สลิป TrueMoney Wallet ​

Verify.TruewalletBy... คืน *TrueWalletResult ฟิลด์ชั้นบนสุดเหมือนสลิปธนาคาร ส่วน RawSlip มีข้อมูลดังนี้

ฟิลด์ชนิดความหมายตัวอย่าง
RawSlip.TransactionIDstringเลขรายการของ TrueMoney"50012345678901"
RawSlip.Datestringเวลาที่ชำระ รูปแบบ ISO 8601"2026-10-26T14:30:00+07:00"
RawSlip.Amountfloat64ยอดเงิน หน่วยเป็นบาท500
RawSlip.Sender.Namestringชื่อผู้โอน"นาย ผู้โอน ทดสอบ"
RawSlip.Receiver.Namestringชื่อผู้รับ"นาย รับเงิน ทดสอบ"
RawSlip.Receiver.Phonestringเบอร์โทรผู้รับ ปิดบางหลักไว้"08x-xxx-4567"

งานเบื้องหลังและ event ของเว็บฮุค ​

Jobs.Get และ WaitForJob คืน *Job ส่วนเว็บฮุคได้รับ *WebhookEvent

ฟิลด์JobWebhookEventความหมาย
JobIDมีมีID ของงานที่ SubmitBank หรือ SubmitBankBatch คืนมา
BatchIDมีมีID ของชุด เมื่อสลิปถูกส่งมาเป็นชุด
Statusqueued, processing, retrying, done หรือ failedsuccess, not_found หรือ failedงานอยู่ขั้นไหน หรือจบด้วยผลอะไร
Result.Statusมีไม่มีsuccess หรือ not_found เมื่องานเป็น done แล้ว
Result.Data, Dataมีมี*BankSlipResult แบบเดียวกับการเรียกตรง เมื่อตรวจสลิปได้
Error.Code, Error.Messageมีมีสาเหตุที่ตรวจไม่สำเร็จ เมื่อสถานะเป็น failed
Timestampไม่มีมีเวลาที่ได้ผล

ข้อมูลประกอบของ response ​

ผลการตรวจทุกตัวมีฟิลด์ Meta ที่เก็บ HTTP Status, RequestID, ตัวนับ RateLimit และ Headers ของ response ค่านี้ไม่ได้เป็นส่วนของข้อมูล json.Marshal(result) จึงไม่รวม Meta ไว้ แนบ Meta.RequestID มาด้วยเมื่อติดต่อซัพพอร์ตเรื่องการเรียกครั้งนั้น

การจัดการ error ​

error ของ SDK ทุกตัว implement interface easyslip.EasySlipError และหา type ได้ด้วย errors.As ยกเว้นกรณีที่คุณยกเลิก context เอง ซึ่ง WaitForJob จะคืน error ของ context นั้นตรง ๆ ส่วนเมธอดอื่นจะคืน *NetworkError ที่ตรงกับ error นั้นเมื่อเช็กด้วย errors.Is error ที่มาจาก API จะ unwrap ลงไปถึง *easyslip.ApiError ซึ่งมี Status, Code (เช่น SLIP_NOT_FOUND), Message, RequestID และ RetryAfter บาง type จะ unwrap ไปเป็นอีก type ได้แก่ QuotaExceededError เป็น PermissionDeniedError ส่วน InvalidImageError, InvalidUrlError และ DuplicateSlipError เป็น InvalidRequestError ส่วน SlipNotFoundError และ SlipPendingError เป็น NotFoundError ส่วน ServiceUnavailableError เป็น ServerError และ RequestTimeoutError เป็น NetworkError ดูความหมายของ Code ทุกตัวที่ API คืนได้ที่รหัส Error

ถ้า SDK รู้จัก code ของ error จะเลือก type จาก code ก่อน ถ้าไม่รู้จักจะเลือกจาก HTTP status ในตาราง และ 4xx อื่นจะได้ *ApiError

type ของ errorHTTP statusเกิดเมื่อควรทำอย่างไร
*AuthenticationError401ไม่มี API key, key ผิด หรือ key ถูกยกเลิกแล้วตรวจ key ที่ส่งเข้าไปหรือ EASYSLIP_API_KEY ถ้าจำเป็นให้สร้าง key ใหม่ใน developer portal
*PermissionDeniedError403key ถูกต้องแต่ไม่มีสิทธิ์ทำคำขอนี้ เช่น สาขาถูกปิด แพ็กเกจหมดอายุ หรือ IP ของเซิร์ฟเวอร์ไม่อยู่ในรายการที่อนุญาตดู Code แล้วแก้การตั้งค่านั้นใน developer portal
*QuotaExceededError403โควต้าหมด คำขอนี้ยังไม่ถูกหักโควต้าเติมโควต้าหรือเปลี่ยนแพ็กเกจ แล้วส่งคำขอใหม่
*InvalidRequestError400API ไม่รับค่าบางอย่างในคำขอ เช่น ข้อความคิวอาร์หรือค่าตัวเลือกแก้ค่าที่ Message บอก ส่งคำขอเดิมซ้ำจะได้ error เดิม
*InvalidImageError400อ่านคิวอาร์ของสลิปในรูปไม่ได้ ชนิดไฟล์ไม่รองรับ หรือไฟล์ใหญ่เกินไปขอให้ลูกค้าส่งรูป JPEG, PNG, GIF หรือ WebP ที่ชัดและไม่เกิน 4 MB
*InvalidUrlError400URL ของรูปหรือ CallbackURL ชี้ไปที่ที่อยู่ภายใน ไม่ได้ใช้ http หรือ https หรือเปิดไม่ได้ใช้ URL แบบ https ที่เปิดได้จากภายนอก
*DuplicateSlipError400เฉพาะ API v1 ที่ใส่ CheckDuplicate: สลิปนี้เคยถูกตรวจแล้ว Slip คือผลการตรวจครั้งแรกถือว่าได้รับยอดนี้ไปแล้ว อย่ายืนยันซ้ำ
*NotFoundError404ไม่พบบัญชีธนาคาร งาน หรือรายการที่ขอตรวจ ID ที่ส่งเข้าไป
*SlipNotFoundError404ธนาคารไม่มีรายการโอนที่ตรงกับสลิปนี้อย่ารับยอด และขอสลิปตัวจริงจากลูกค้า
*SlipPendingError404ธนาคารยังไม่เผยแพร่รายการโอนนี้ มักเกิดกับสลิปธนาคารกรุงเทพที่เพิ่งโอนรอประมาณหนึ่งนาทีแล้วตรวจใหม่
*ConflictError409บัญชีรับเงินที่กำลังเพิ่มมีอยู่แล้วใช้บัญชีเดิม หาได้ด้วย BankAccounts.IterateAll
*RateLimitError429ส่งคำขอถี่เกินไป RetryAfter บอกว่าต้องรอกี่วินาทีลดจำนวนคำขอที่ส่งพร้อมกัน SDK ลองใหม่ให้แล้วก่อนคืน error นี้
*ServerError500, 502, 504; 5xx อื่นยกเว้น 503เกิดปัญหาที่ฝั่ง EasySlipลองใหม่ภายหลัง ถ้ายังเกิดซ้ำให้ติดต่อซัพพอร์ตพร้อม RequestID
*ServiceUnavailableError503บริการไม่พร้อมใช้งานชั่วคราว เช่น ช่วงต่ออายุโควต้าลองใหม่ในอีกไม่กี่นาที
*ApiError4xx อื่น ๆAPI ตอบ error ด้วย status ที่ไม่มี type เฉพาะเก็บ log Status, Code และ RequestID ถ้ายังเกิดอยู่ให้ติดต่อซัพพอร์ต
*NetworkErrorเครือข่ายไม่ได้รับคำตอบเพราะ DNS การเชื่อมต่อ หรือ TLS ล้มเหลวตรวจเครือข่ายหรือ proxy แล้วลองใหม่
*RequestTimeoutErrorเครือข่ายไม่ได้รับคำตอบภายในเวลาที่ตั้งไว้ลองใหม่หรือเพิ่ม timeout การตรวจสลิปครั้งนั้นอาจถูกนับโควต้าไปแล้ว
*InputErrorก่อนส่งคำขอSDK เจอปัญหาก่อนส่ง เช่น ไม่มี API key ข้อความคิวอาร์ว่าง รูปเกิน 4 MB หรือไฟล์ไม่ใช่รูป Code บอกสาเหตุแก้ค่าที่ส่งเข้าไป
*WebhookSignatureErrorก่อนส่งคำขอเว็บฮุคไม่มีลายเซ็น หรือลายเซ็นไม่ตรงตอบเว็บฮุคนั้นด้วย status 400
*JobWaitTimeoutErrorก่อนส่งคำขอWaitForJob รอจนครบ Timeout แล้ว LastJob คือสถานะล่าสุดที่เห็นรอนานขึ้น หรือรอผลทางเว็บฮุคแทน
*BatchSubmitErrorก่อนส่งคำขอSubmitBankMany หยุดกลางทาง Submitted คือชุดที่ส่งไปแล้ว FailedChunk คือลำดับของชุดที่ล้มเหลว นับจาก 0 และ Cause คือ error ของชุดนั้นส่งชุดที่ล้มเหลวและชุดถัดไปใหม่ ชุดที่ส่งไปแล้วยังทำงานต่อ

ตัวอย่างนี้ตรวจสลิปที่ลูกค้าอัปโหลดตอนชำระเงิน แล้วแปลง error แต่ละแบบเป็นข้อความที่แสดงให้ลูกค้าเห็น

go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func checkPayment(ctx context.Context, client *easyslip.Client, upload []byte, orderTotal float64) string {
	result, err := client.Verify.BankByImage(ctx, upload, &easyslip.VerifyOptions{
		MatchAmount:    easyslip.Some(orderTotal),
		MatchAccount:   easyslip.Some(true),
		CheckDuplicate: easyslip.Some(true),
	})
	var (
		notFound    *easyslip.SlipNotFoundError
		pending     *easyslip.SlipPendingError
		badImage    *easyslip.InvalidImageError
		quota       *easyslip.QuotaExceededError
		rateLimited *easyslip.RateLimitError
		apiError    *easyslip.ApiError
	)
	switch {
	case errors.As(err, &notFound):
		return "ไม่พบรายการโอนนี้ที่ธนาคาร"
	case errors.As(err, &pending):
		return "ธนาคารกำลังดำเนินการรายการนี้ ลองใหม่ในอีกหนึ่งนาที"
	case errors.As(err, &badImage):
		return "กรุณาอัปโหลดรูปถ่ายหรือภาพหน้าจอสลิปที่ชัดเจน"
	case errors.As(err, &quota), errors.As(err, &rateLimited):
		log.Printf("ใช้ EasySlip ถึงขีดจำกัดแล้ว: %v", err)
		return "ตอนนี้ยังตรวจสลิปไม่ได้ กรุณาลองใหม่อีกครั้ง"
	case errors.As(err, &apiError):
		requestID := ""
		if apiError.RequestID != nil {
			requestID = *apiError.RequestID
		}
		log.Printf("EasySlip ตอบ error: %v (request %s)", apiError, requestID)
		return "ตรวจสลิปไม่สำเร็จ กรุณาลองใหม่"
	case err != nil:
		log.Printf("ติดต่อ EasySlip ไม่ได้: %v", err)
		return "ตรวจสลิปไม่สำเร็จ กรุณาลองใหม่"
	}

	duplicate, checked := result.IsDuplicate.Value()
	amountMatched, _ := result.IsAmountMatched.Value()
	account, _ := result.MatchedAccount.Value()
	switch {
	case !checked || duplicate:
		return "สลิปนี้ถูกใช้ไปแล้ว"
	case !amountMatched:
		return fmt.Sprintf("สลิปนี้โอน %v บาท", result.AmountInSlip)
	case account == nil:
		return "สลิปนี้ไม่ได้โอนเข้าบัญชีของเรา"
	}
	return "ได้รับการชำระเงินแล้ว"
}

func main() {
	client, err := easyslip.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	upload, err := os.ReadFile("slip.jpg")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(checkPayment(context.Background(), client, upload, 1500))
}

err.Error() ไม่มี API key อยู่ในข้อความเลย จึงเก็บ log ของ error ได้ตามที่ได้รับมา

SDK ลองใหม่ให้เมื่อไร ​

SDK ลองส่งคำขอที่ล้มเหลวใหม่ได้สูงสุดตามค่า WithMaxRetries (ค่าเริ่มต้น 2 ครั้ง) ก่อนลองใหม่ครั้งแรกจะรอประมาณครึ่งวินาที แล้วเพิ่มเวลารอเป็นสองเท่าทุกครั้ง ไม่เกิน 8 วินาที ถ้า API ส่ง Retry-After มา SDK จะรอตามนั้นแทน แต่ถ้าต้องรอนานกว่า 60 วินาทีจะคืน error ทันที

คำขอลองใหม่เมื่อไม่ลองใหม่เมื่อ
การอ่านข้อมูลและการผูกบัญชี: GetInfo, Banks.List, BankAccounts.List, ListAll, Get และ Link, Jobs.Get, V1.GetMe, V1.GenerateQRเครือข่ายล้มเหลว หมดเวลา หรือ API ตอบ 429, 500, 502, 503 หรือ 504API ตอบ 400, 401, 403, 404 หรือ 409
การตรวจสลิปและการส่งงานเบื้องหลัง ซึ่งใช้โควต้าAPI ตอบ 429 หรือเชื่อมต่อไม่สำเร็จก่อนคำขอจะออกจากเซิร์ฟเวอร์คำขอส่งออกไปแล้ว แล้วหมดเวลาหรือได้คำตอบ 5xx
การแก้ไขบัญชีรับเงิน: Create, Update และ UnlinkAPI ตอบ 429 หรือเชื่อมต่อไม่สำเร็จก่อนคำขอจะออกจากเซิร์ฟเวอร์คำขอส่งออกไปแล้ว แล้วหมดเวลาหรือได้คำตอบ 5xx

การตรวจสลิปที่ส่งถึง API แล้วอาจถูกนับโควต้าไปแล้ว แม้จะไม่ได้รับคำตอบกลับมา การลองใหม่อาจทำให้ถูกหักโควต้าซ้ำ SDK จึงให้คุณเป็นคนตัดสินใจเอง ถ้าจะส่งใหม่ ให้ใส่ CheckDuplicate ไว้ด้วย ถ้าครั้งแรกถูกนับโควต้าไปแล้ว ครั้งที่สองจะได้ผลเดิมที่บันทึกไว้พร้อม IsDuplicate เป็น true และไม่ใช้โควต้า

เว็บฮุค ​

เมื่องานเบื้องหลังเสร็จ EasySlip จะส่งคำขอ POST ไปที่ CallbackURL ของคุณ ถ้าสาขาตั้ง Webhook Secret ไว้ คำขอจะมี header X-EasySlip-Signature ให้ตรวจลายเซ็นกับ body ดิบก่อนแปลงหรือเชื่อข้อมูลใด ๆ ในนั้น ดูรายละเอียดของคำขอทั้งหมดได้ที่ Webhook Callback

go
package main

import (
	"io"
	"log"
	"net/http"
	"os"
	"time"

	easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)

func main() {
	secret := os.Getenv("EASYSLIP_WEBHOOK_SECRET")
	if secret == "" {
		log.Fatal("กรุณาตั้งค่า EASYSLIP_WEBHOOK_SECRET")
	}

	mux := http.NewServeMux()
	mux.HandleFunc("POST /webhooks/easyslip", func(w http.ResponseWriter, r *http.Request) {
		rawBody, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 4<<20)) // body ตามที่ได้รับมาทุก byte
		if err != nil {
			http.Error(w, "invalid body", http.StatusBadRequest)
			return
		}
		event, err := easyslip.ConstructWebhookEvent(rawBody, r.Header.Get(easyslip.WebhookSignatureHeader), secret)
		if err != nil {
			http.Error(w, "invalid webhook", http.StatusBadRequest)
			return
		}
		if event.Status == "success" && event.Data != nil {
			log.Printf("ตรวจผ่าน %s %s %v", event.JobID, event.Data.RawSlip.TransRef, event.Data.AmountInSlip)
		} else {
			code := ""
			if e, ok := event.Error.Value(); ok {
				code = e.Code
			}
			log.Printf("ตรวจไม่ผ่าน %s %s %s", event.JobID, event.Status, code)
		}
		w.WriteHeader(http.StatusNoContent)
	})

	server := &http.Server{Addr: ":8000", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
	log.Fatal(server.ListenAndServe())
}

handler นี้ตอบ 400 เมื่อไม่มีลายเซ็นหรือลายเซ็นไม่ตรง และเมื่อ body ที่ลงลายเซ็นมาไม่ใช่ event ของเว็บฮุค ถ้าใช้ Gin, Echo หรือเฟรมเวิร์กอื่น ให้อ่าน body ดิบด้วย io.ReadAll ก่อนจะ bind JSON การแปลง JSON แล้วแปลงกลับจะทำให้ bytes เปลี่ยน แล้วลายเซ็นจะไม่ตรง

ฟังก์ชันใช้ทำอะไร
ConstructWebhookEvent(rawBody, signature, secret)ตรวจลายเซ็นแล้วคืน *WebhookEvent ที่แปลงแล้ว คืน *WebhookSignatureError เมื่อไม่มีลายเซ็นหรือลายเซ็นไม่ตรง และคืน *InputError เมื่อ body ไม่ใช่ JSON object ที่มี jobId, status และ timestamp เป็นข้อความ
VerifyWebhookSignature(rawBody, signature, secret)คืน true เมื่อลายเซ็นตรง และ false เมื่อไม่ตรง
ParseWebhookEvent(rawBody)แปลง event โดยไม่ตรวจลายเซ็น และคืน *InputError เมื่อ body ไม่ใช่ event ใช้หลังจากตรวจลายเซ็นเองแล้วเท่านั้น

secret คือ Webhook Secret ของสาขา ต้องส่งทุกครั้งที่เรียก ตั้งค่าได้ใน developer portal ถ้าไม่ได้ตั้งไว้ EasySlip จะส่ง callback มาโดยไม่มีลายเซ็น และ ConstructWebhookEvent จะไม่รับ จึงควรตั้งให้เรียบร้อยก่อนเปิดใช้งานจริง

กติกาการส่งเว็บฮุค

  • ตอบกลับด้วย status 2xx ภายใน 5 วินาที บันทึก event ไว้ก่อนแล้วค่อยทำงานที่ใช้เวลานานทีหลัง เช่น ใน goroutine ที่รับงานจาก queue
  • ถ้าตอบด้วย status อื่นที่ไม่ใช่ 2xx หรือตอบไม่ทัน EasySlip จะส่ง event เดิมอีกครั้งหลังผ่านไปประมาณ 10, 30 และ 120 วินาที
  • EasySlip อาจส่ง event เดิมมาซ้ำได้ ให้ใช้ JobID ข้าม event ที่จัดการไปแล้ว
  • ถ้าส่งไม่สำเร็จทุกครั้ง Jobs.Get ยังคืนผลได้ประมาณ 7 วัน

ความปลอดภัยและข้อมูลส่วนบุคคล ​

  • เก็บ API key ไว้ฝั่งเซิร์ฟเวอร์เท่านั้น อย่าใส่ในหน้าเว็บ แอปมือถือ หรือ repository สาธารณะ เพราะใครได้ key ไปก็ใช้โควต้าของคุณได้
  • อ่าน key จาก EASYSLIP_API_KEY หรือไฟล์ secret แทนการเขียนลงในโค้ด
  • SDK ส่ง key ไปใน header Authorization เท่านั้น และลบ key ออกจากข้อความ error และ header ที่ส่งคืนให้คุณ จึงเก็บ log ของ error ได้อย่างปลอดภัย ส่วน SensitiveRawBody() คืน body ของ error ฉบับเดิม อย่าเก็บค่านี้ลง log
  • สลิปมีชื่อ เลขบัญชี และยอดเงิน ซึ่งเป็นข้อมูลส่วนบุคคล เก็บเฉพาะที่จำเป็น ดูแลเหมือนข้อมูลลูกค้าอื่น ๆ และปฏิบัติตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล (PDPA)
  • ตรวจลายเซ็นของเว็บฮุคทุกครั้งก่อนนำข้อมูลไปใช้

เวอร์ชันและนโยบายการรองรับ ​

  • การออกเวอร์ชันใช้ semantic versioning เวอร์ชัน minor และ patch จะไม่ทำให้โค้ดเดิมพัง การเปลี่ยนแปลงที่กระทบโค้ดเดิมจะออกเป็นเวอร์ชัน major ใหม่ พร้อมวิธีอัปเกรดใน changelog
  • SDK รองรับ Go เวอร์ชันที่โปรเจกต์ Go ยังดูแลอยู่ เมื่อ Go เวอร์ชันใดหมดอายุการดูแล SDK เวอร์ชันถัดไปอาจเลิกรองรับเวอร์ชันนั้น และจะแจ้งไว้ใน changelog
  • changelog และหน้า releases รวบรวมทุกเวอร์ชันและสิ่งที่เปลี่ยน

อ่านเพิ่มเติม ​

  • โฟลเดอร์ examples มีโปรแกรมที่รันได้จริงครบทุกเมธอด
  • ภาพรวม API v2 และรหัส Error อธิบายทุก endpoint ฟิลด์ และรหัสข้อผิดพลาด
  • คำถามเรื่องบัญชี โควต้า หรือการเรียกเก็บเงิน ติดต่อ EasySlip ได้ทาง LINE @ai-easy โทร 02-114-8806 หรือหน้าติดต่อเรา โดยแนบ RequestID ของการเรียกครั้งนั้นมาด้วย
  • แจ้งบั๊กหรือขอฟีเจอร์ได้ที่ issue ห้ามโพสต์ API key หรือสลิปจริง ส่วนปัญหาด้านความปลอดภัยให้แจ้งแบบส่วนตัวตามที่ SECURITY.md อธิบายไว้

Bank Slip Verification API for Thai Banking