1. ภาพรวมการเชื่อมต่อ
| ข้อมูล | ค่า |
|---|---|
| Endpoint หลัก | https://toctoico.com/chargingws/v2 |
| API ตารางค่าธรรมเนียม | https://toctoico.com/chargingws/v2/getfee?partner_id=YOUR_PARTNER_ID |
| Method | POST |
| รูปแบบข้อมูล | application/json หรือ application/x-www-form-urlencoded |
| คำสั่งที่รองรับ | charging, check |
2. เงื่อนไขก่อนเรียก API
รองรับโปรโตคอล Nappay: เปลี่ยน URL เป็นเว็บไซต์นี้ และใช้ Partner ID/Key กับรหัสกระเป๋าของ Merchant ที่นี่ รายการสินค้า ราคา และเงื่อนไขดำเนินการมาจากระบบนี้
เชื่อมต่อกับระบบนี้โดยตรงด้วย Merchant ของบัญชีคุณ จำนวนเงินทั้งหมดเป็นดองเวียดนาม (VND) ให้ส่งเป็นจำนวนเต็มไม่มีเครื่องหมายคั่น
- เข้าสู่ระบบ → เชื่อมต่อ API / Merchant, สร้าง Merchant ให้ตรงบริการแล้วเปิดใช้งาน
- รับ Partner ID, Partner Key และรหัสกระเป๋าจาก Merchant นั้น หากตั้งค่า IP ที่อนุญาต ให้ระบุ IP สาธารณะของเซิร์ฟเวอร์ที่ส่งคำขอ
- ตั้งค่า URL Callback HTTPS ของระบบรับผล เลือก POST JSON หรือ GET ตาม Merchant
- ดึงหมวดหมู่/ค่าธรรมเนียมและตรวจสอบยอดก่อน จากนั้นจึงเชื่อมต่อการสร้างคำสั่งซื้อ การค้นหา และการกระทบยอด
Partner Key ใช้บนเซิร์ฟเวอร์ของคุณเท่านั้น อย่าใส่คีย์ใน JavaScript เบราว์เซอร์ แอปสาธารณะ หรือ URL ค่า YOUR_PARTNER_ID, YOUR_PARTNER_KEY, YOUR_WALLET ด้านล่างเป็นช่องสำหรับข้อมูล Merchant ของคุณบนเว็บนี้
Endpoint แลกบัตร Topup และซื้อรหัสบัตร
| บริการ | Endpoint | รูปแบบ |
|---|---|---|
| แลกบัตรเติมเงิน | https://toctoico.com/chargingws/v2 | POST JSON หรือ form-urlencoded |
| เติมเงิน Topup | https://toctoico.com/api/rechargews | POST JSON หรือ form-urlencoded |
| ซื้อรหัสบัตร | https://toctoico.com/api/cardws | POST JSON หรือ form-urlencoded |
| ตารางค่าธรรมเนียมแลกบัตร | https://toctoico.com/chargingws/v2/getfee?partner_id=YOUR_PARTNER_ID | GET |
| หมวดหมู่รหัสบัตร | https://toctoico.com/api/cardws/products?partner_id=YOUR_PARTNER_ID | GET |
3. ลายเซ็นยืนยันตัวตน
ลงนาม MD5 ด้วยสตริง UTF-8 ที่ต่อกันตามลำดับด้านล่าง โดยไม่เพิ่มจุด ช่องว่าง หรืออักขระคั่น ส่งสตริง MD5 แบบ hex และใช้ command ตัวพิมพ์เล็กตามเอกสาร
| กลุ่มคำสั่ง | สตริงสำหรับแฮช MD5 |
|---|---|
| แลกบัตร: charging | partner_key + code + command + partner_id + request_id + serial + telco |
| แลกบัตร: check | partner_key + command + partner_id + request_id |
| แลกบัตร: ยังรองรับลายเซ็นเดิม | partner_key + code + serial |
| Topup: topup, getstatus | partner_key + partner_id + command + request_id |
| Topup: productlist, getbalance, feetopup, canceltopup | partner_key + partner_id + command |
| รหัสบัตร: buycard, redownload | partner_key + partner_id + command + request_id |
| รหัสบัตร: getbalance, checkavailable | partner_key + partner_id + command |
| หมวดหมู่รหัสบัตร, getfee | ยืนยัน Merchant/IP ผ่าน partner_id ขณะนี้ endpoint ไม่ต้องใช้ sign |
| ตรวจสอบ serial /api/checkcard | partner_key + serial |
hashlib.md5((partner_key + partner_id + command + request_id).encode("utf-8")).hexdigest()Merchant แต่ละรายใช้ได้เฉพาะประเภท API ที่ลงทะเบียนไว้ (Charging, Recharge, Softcard หรือ All) ข้อผิดพลาดการยืนยันตัวตน/IP อาจปรากฏใน JSON แม้ HTTP เป็น 200
4. พารามิเตอร์และการตอบกลับสำหรับแลกบัตร
POST /chargingws/v2
ส่งบัตรด้วย command=charging ตรวจสอบด้วย command=check พร้อม partner_id, request_id และ sign ตามตารางด้านบน โดยไม่ต้องส่งข้อมูลบัตรใหม่ ลายเซ็นเดิม MD5(partner_key + code + serial) ยังรองรับเมื่อส่ง code และ serial มาด้วย
| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| partner_id, sign | จำเป็น | Merchant และลายเซ็นของบริการแลกบัตร |
| command | จำเป็น | charging หรือ check |
| request_id | จำเป็น | รหัสคำขอไม่ซ้ำต่อ Merchant; check ใช้รหัสเดิม |
| telco | Khi charging | รหัสเครือข่ายตัวพิมพ์ใหญ่ ดึงจาก getfee |
| amount | Khi charging | มูลค่าบัตรที่ระบุ สามารถไม่ส่งเมื่อ check |
| code, serial | Khi charging | สตริงรหัสบัตร/ซีเรียล ต้องคงเลข 0 ด้านหน้า เมื่อ check ให้ส่งสตริงเดิมกลับให้ตรงทุกตัว |
{
"partner_id": "YOUR_PARTNER_ID",
"command": "charging",
"request_id": "CHARGE_DEMO_001",
"telco": "VIETTEL",
"amount": 100000,
"code": "YOUR_CARD_CODE",
"serial": "YOUR_CARD_SERIAL",
"sign": "MD5_THEO_CONG_THUC_CHARGING"
}ตรวจสอบธุรกรรมอีกครั้ง
{
"partner_id": "YOUR_PARTNER_ID",
"command": "check",
"request_id": "CHARGE_DEMO_001",
"sign": "MD5_PARTNER_KEY_CHECK_PARTNER_ID_REQUEST_ID"
}คำตอบธุรกรรม
{
"trans_id": 123,
"request_id": "CHARGE_DEMO_001",
"status": 99,
"message": "PENDING",
"declared_value": 100000,
"value": 0,
"amount": 0,
"telco": "VIETTEL",
"code": "YOUR_CARD_CODE",
"serial": "YOUR_CARD_SERIAL",
"callback_sign": "MD5_PARTNER_KEY_CODE_SERIAL"
}| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| trans_id, request_id | ได้รับ | รหัสธุรกรรมระบบและรหัสคำขอของคุณ |
| declared_value | ได้รับ | มูลค่าบัตรที่ระบุไว้ |
| value | ได้รับ | มูลค่าบัตรจริงที่ยืนยันแล้ว |
| amount | ได้รับ | ยอดสุทธิเข้ากระเป๋าหลังประมวลผล |
| status, message | ได้รับ | สถานะบัตรหรือข้อผิดพลาด ให้ตรวจทั้งสองค่าเสมอ |
| status | ความหมาย |
|---|---|
| 99 | รออยู่ ยังไม่เพิ่มเงินเหมือนคำสั่งซื้อที่สำเร็จ |
| 1 | บัตรใช้ได้ สำเร็จ |
| 2 | บัตรใช้ได้แต่มูลค่าไม่ตรง |
| 3 | บัตรผิดพลาด / ไม่ถูกต้อง |
| 4 | ข้าม / บำรุงรักษาตามผลการประมวลผล |
| 100 | ข้อผิดพลาดคำขอ |
| 101 | ไม่พบธุรกรรม |
| 102 | การยืนยันตัวตน IP หรือข้อมูลไม่ถูกต้อง |
| 104 | คำสั่งไม่ถูกต้อง |
ตารางค่าธรรมเนียมตาม Merchant
GET /chargingws/v2/getfee?partner_id=YOUR_PARTNER_ID. การตอบกลับเป็นรายการแบบแบนที่มี telco, value, fees และ penalty ซึ่งเข้ากันได้กับ Nappay ระบบเดิมที่ต้องการ object แบบจัดกลุ่มสามารถเพิ่ม format=grouped ได้ fees คือค่าธรรมเนียมลูกค้าตามกลุ่มบัญชี โดยคืนเฉพาะประเภทบัตร/มูลค่าบัตรที่เปิดใช้งาน อย่ากำหนดหมวดหมู่หรือค่าธรรมเนียมตายตัวตามตัวอย่างเอกสาร
[
{
"telco": "VIETTEL",
"value": 100000,
"fees": "18.5",
"penalty": "50"
}
]ค่าธรรมเนียมกลุ่มถูกกำหนดตอนรับบัตร หากมูลค่าผิด (status=2) จะปรับ 50% จากยอดรับหลังหักค่าธรรมเนียมกลุ่ม: ยอดรับ = มูลค่าที่ต้นทางยืนยัน × (100 − ค่าธรรมเนียมกลุ่ม)% × 50% ปัดลงเป็นหน่วย ดอง ตัวอย่าง 10.000đ ค่าธรรมเนียมกลุ่ม 26,8% รับ 3.660đ ใช้ amount ของผลลัพธ์สุดท้ายบันทึกยอด อย่าคำนวณใหม่จากมูลค่าที่แจ้ง รายการแบบทำเองอาจยังเป็นสถานะ 99 จนผู้ดำเนินการยืนยัน; การค้นหา/callback เป็นหลักฐานอัปเดตสุดท้าย
4. พารามิเตอร์และการตอบกลับสำหรับ Topup
POST /api/rechargews
| command | พารามิเตอร์เฉพาะ | ผลลัพธ์ |
|---|---|---|
| productlist | partner_id, sign | หมวดหมู่และราคาตามบัญชี |
| getbalance | partner_id, sign | ยอดกระเป๋า VND |
| topup | request_id, service_code, amount, qty=1, account_info | รับคำสั่งซื้อและส่ง order_code กลับ; ยังไม่ได้หมายความว่าเติมเงินสำเร็จ |
| getstatus | request_id และ order_code ของคำสั่งซื้อเดิม | ตรวจสอบคำสั่งซื้อที่ตรงกับ Merchant |
| feetopup | partner_id, sign | รายการ key, value, price, discount ตามบัญชี |
| canceltopup | request_id, order_code, sign | ยกเลิกได้เฉพาะคำสั่งซื้ออัตโนมัติที่ยังไม่ถูกส่งไปประมวลผล คำสั่งซื้อที่ส่งแล้วหรือยังไม่ทราบผลจะถูกปฏิเสธ |
| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| partner_id, command, sign | ทุกคำสั่ง | ลายเซ็นตามตารางการยืนยันตัวตน |
| request_id | topup, getstatus | รหัสคำขอของคุณ ยาวไม่เกิน 100 อักขระ ใช้ตัวอักษร/ตัวเลขและ _ . : - สำหรับหมวดหมู่ที่ตั้งค่าไว้ |
| service_code | topup | ใช้ค่าจาก productlist ให้ตรง ห้ามใช้ชื่อที่แสดง |
| amount | topup | มูลค่าบัตรหรือจำนวนเงินที่ต้องการเติมก่อนส่วนลด |
| qty | topup | เท่ากับ 1 เสมอ |
| account_info | topup | Object ที่ใช้ key ตรงตาม options ของสินค้า; ค่าช่องข้อมูลเป็น string |
| order_code | getstatus | รหัสที่ระบบคืนเมื่อสร้างคำสั่งซื้อ ห้ามใช้รหัส Nappay หรือ request_id แทน |
1. ดึงหมวดหมู่และข้อมูลที่ลูกค้าต้องกรอก
{
"partner_id": "YOUR_PARTNER_ID",
"command": "productlist",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_PRODUCTLIST"
}อ่าน status=success และ data เป็นรายการสินค้า แต่ละสินค้ามี service_code, name, options และ items โดย options ระบุ key, name, type ชนิด select มี choices ช่อง password/otp ให้เก็บบนเซิร์ฟเวอร์และส่งผ่าน HTTPS เท่านั้น
{
"status": "success",
"data": [
{
"service_code": "DEMO_GAME",
"name": "Sản phẩm minh họa",
"options": [
{
"key": "game_id",
"name": "ID nhân vật",
"type": "text"
}
],
"amount_mode": "fixed",
"amount_limits": null,
"items": [
{
"value": 100000,
"name": "Gói 100.000đ",
"price": "92500",
"discount": 7.5
}
]
}
]
}price คือยอดชำระ ส่วน value คือมูลค่าหน้าบัตร discount แสดงส่วนลดของบัญชี คำนวณด้วย (value − price) / value × 100 ห้ามบวกกำไรหรือหักส่วนลดจาก price ซ้ำ
2. แพ็กเกจคงที่หรือให้ผู้ใช้กรอกจำนวนเงินเอง
amount_mode=fixed: เลือก value หนึ่งรายการจาก items ส่วน amount_mode=amount: อ่าน amount_limits.minimum, maximum และ multiple แล้วส่ง amount ในช่วงที่อนุญาตและหาร multiple ลงตัว items อาจมีแพ็กเกจทางเทคนิค value=1, price=1 ซึ่งไม่ใช่แพ็กเกจขาย 1đ ให้ใช้ amount_mode และ amount_limits สร้างฟอร์ม โหลดรายการใหม่เมื่อเปลี่ยนการตั้งค่า/ราคา ผลการรับคำสั่งซื้อคือราคาที่สรุปแล้ว
3. ส่งคำสั่งซื้อ
{
"partner_id": "YOUR_PARTNER_ID",
"command": "topup",
"request_id": "TOPUP_DEMO_001",
"service_code": "DEMO_GAME",
"amount": 100000,
"qty": 1,
"account_info": {
"game_id": "PLAYER_ID"
},
"sign": "MD5_PARTNER_KEY_PARTNER_ID_TOPUP_REQUEST_ID"
}JSON ส่ง account_info เป็น object หากใช้ form-urlencoded ให้ส่ง account_info เป็นสตริง JSON หรือ account_info[game_id]=PLAYER_ID ห้ามส่งสองรูปแบบที่ขัดแย้งกัน เกมนี้เป็นเพียงตัวอย่าง ต้องดึงฟิลด์จริงจาก productlist อย่าสมมติว่าทุกเกมใช้ game_id
{
"status": "success",
"data": {
"time": "2026-10-18 10:00:00",
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"pay_amount": 92500,
"discount": 7.5,
"service_code": "DEMO_GAME",
"amount": 100000,
"status": "pending"
}
}4. ค้นหาและตรวจสอบยอดคงเหลือ
{
"partner_id": "YOUR_PARTNER_ID",
"command": "getstatus",
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_GETSTATUS_REQUEST_ID"
}data.status: pending = กำลังดำเนินการ; failed = ไม่สำเร็จ เมื่อสำเร็จ ผลตอบกลับการสร้างคำสั่งซื้อในรายการภายในอาจใช้ success ส่วน getstatus และ callback ใช้ completed ค่า status=success ชั้นนอกบอกเพียงว่า API รับ/ตรวจสอบคำขอได้ ให้ดู data.status เพื่อสรุปผลคำสั่งซื้อ getstatus ส่ง request_value เป็นมูลค่าหน้าบัตร ส่วน amount ของคำสั่งซื้อที่มีใบเสนอราคาภายในคือยอดที่ชำระแล้ว
{
"status": "success",
"data": {
"account": {
"game_id": "PLAYER_ID"
},
"request_value": 100000,
"amount": 92500,
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"status": "completed"
}
}{
"partner_id": "YOUR_PARTNER_ID",
"command": "getbalance",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_GETBALANCE"
}{
"status": "success",
"data": {
"balance": 1000000,
"currency": "VND"
}
}4. พารามิเตอร์และการตอบกลับสำหรับซื้อรหัสบัตร
POST /api/cardws
| คำสั่ง / เส้นทาง | พารามิเตอร์เฉพาะ | ผลลัพธ์ |
|---|---|---|
| GET /api/cardws/products | partner_id | อาร์เรย์สินค้า; cardvalue มี value และ price |
| checkavailable | service_code, value, qty | stock_available: true/false |
| getbalance | sign; หากส่ง wallet_number ต้องตรงกับ Merchant | balance และ currency_code |
| buycard | request_id, service_code, value, qty, sign | รหัสคำสั่งซื้อและบัตร หรือสถานะรอ |
| redownload | request_id, sign; order_code ไม่บังคับ | ดึงผลของคำสั่งซื้อก่อนหน้าอีกครั้ง |
หมวดหมู่และสินค้าคงเหลือ
curl "https://toctoico.com/api/cardws/products?partner_id=YOUR_PARTNER_ID"[
{
"name": "Thẻ minh họa",
"service_code": "DEMO_CARD",
"cardvalue": [
{
"service_code": "DEMO_CARD",
"value": 100000,
"id": "DEMO_SKU",
"price": 97000
}
]
}
]การตรวจสอบสินค้าคงเหลือเป็นเพียงข้อมูล ณ เวลาที่เรียก ไม่ใช่การสำรองสินค้า คำสั่งซื้อยังต้องได้รับการประมวลผลและตรวจสอบยอดคงเหลือ
{
"partner_id": "YOUR_PARTNER_ID",
"command": "checkavailable",
"service_code": "DEMO_CARD",
"value": 100000,
"qty": 1
}{
"stock_available": true,
"message": "Còn hàng"
}ซื้อและดึงผลซ้ำ
{
"partner_id": "YOUR_PARTNER_ID",
"command": "buycard",
"request_id": "CARD_DEMO_001",
"wallet_number": "YOUR_WALLET",
"service_code": "DEMO_CARD",
"value": 100000,
"qty": 1,
"sign": "MD5_PARTNER_KEY_PARTNER_ID_BUYCARD_REQUEST_ID"
}qty ตั้งแต่ 1 ถึง 100 ขึ้นกับสต็อกและยอดคงเหลือ value คือมูลค่าหน้าบัตร ไม่ใช่ราคาหลังหักส่วนลด service_code และราคาให้นำจากรายการสินค้าตาม Merchant
{
"status": 1,
"message": "Mua thẻ thành công",
"data": {
"request_id": "CARD_DEMO_001",
"order_code": "S_DEMO_ORDER",
"cards": [
{
"serial": "DEMO_SERIAL",
"code": "DEMO_PIN",
"expired": "2027-12-31"
}
]
}
}| status | ความหมาย |
|---|---|
| 1 | เสร็จสมบูรณ์ รับ cards ใน data |
| 2 | คำสั่งซื้อต้นทางกำลังดำเนินการ ให้เก็บ request_id และ order_code ไว้ตรวจสอบ |
| 3 | คำสั่งซื้อต้นทางยกเลิก / คืนเงินแล้ว |
| รหัสอื่นหรือ error | คำขอผิดพลาด โปรดอ่าน message และ HTTP |
{
"partner_id": "YOUR_PARTNER_ID",
"command": "redownload",
"request_id": "CARD_DEMO_001",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_REDOWNLOAD_REQUEST_ID"
}เมื่อรอผลหรือการเชื่อมต่อขาด ให้ใช้ redownload กับรหัสคำสั่งซื้อที่ได้รับก่อน อย่าเปลี่ยน request_id แล้วซื้อใหม่ การเรียก buycard ซ้ำในบางขั้นตอนจะคืนข้อผิดพลาดรหัสซ้ำ จึงใช้ redownload เป็นขั้นตอนหลักในการกระทบยอด เก็บรหัส/PIN บนเซิร์ฟเวอร์ของคุณอย่างปลอดภัย
{
"partner_id": "YOUR_PARTNER_ID",
"command": "getbalance",
"wallet_number": "YOUR_WALLET",
"sign": "MD5_PARTNER_KEY_PARTNER_ID_GETBALANCE"
}{
"balance": 1000000,
"currency_code": "VND"
}5. Callback และการกระทบยอด
ลงทะเบียน URL callback ของคุณใน Merchant บนเว็บนี้ ระบบส่งผลแลกบัตร/Topup ผ่าน POST JSON หรือ GET ตามการตั้งค่า Merchant อาจส่ง callback ซ้ำได้ จึงต้องจัดการรายการซ้ำอย่างปลอดภัยตามรหัสคำสั่งซื้อและสถานะ
| บริการ | ตรวจสอบ callback_sign | ฟิลด์หลัก |
|---|---|---|
| แลกบัตร | MD5(partner_key + code + serial) | status, message, request_id, trans_id, declared_value, value, amount, telco, code, serial |
| Topup | MD5(partner_key + order_code) | status, request_id, order_code, request_value, amount, account |
Callback Topup ใช้ status=completed เมื่อรายการสำเร็จ; request_value คือยอดที่ต้องการเติม และ amount คือยอดชำระหากมีราคาเสนอที่สรุปไว้ การค้นหาอาจคืน data.status=success สำหรับผลเดียวกัน Callback GET แลกบัตรมี card_value เพิ่มด้วย
{
"status": "completed",
"request_id": "TOPUP_DEMO_001",
"order_code": "R_DEMO_ORDER",
"request_value": 100000,
"amount": 92500,
"account": {
"game_id": "PLAYER_ID"
},
"callback_sign": "MD5_PARTNER_KEY_ORDER_CODE"
}- ตรวจลายเซ็นด้วยคีย์ Merchant ที่ตรงกัน เปรียบเทียบอย่างปลอดภัย และตรวจว่ารหัสคำสั่งซื้ออยู่ในระบบของคุณ
- เรียก check/getstatus เพื่อยืนยันสถานะและเทียบมูลค่าบัตร/จำนวนเงินก่อนบันทึกผล ลายเซ็นที่รองรับย้อนหลังไม่ได้ลงนามครอบคลุมทุกฟิลด์สถานะ
- บันทึกภายในธุรกรรมฐานข้อมูล ป้องกันการบันทึก/เพิ่มเงินซ้ำ จากนั้นตอบ HTTP 2xx อย่างรวดเร็ว
- หาก callback ล่าช้าหรือสูญหาย ให้ค้นหาตามรอบ อย่าถือว่าการเชื่อมต่อขาดคือความล้มเหลว และอย่าสร้างคำสั่งซื้อใหม่
คิว Callback ลองใหม่ได้สูงสุด 12 ครั้ง โดยเพิ่มเวลารอแต่ละครั้ง Callback ของ Topup ยังขึ้นอยู่กับการตั้งค่า Callback ของสินค้า การซื้อรหัสบัตรรับผลผ่าน buycard/redownload
import hashlib, hmac
expected = hashlib.md5((partner_key + payload["order_code"]).encode()).hexdigest()
valid = hmac.compare_digest(expected, str(payload.get("callback_sign", "")).lower())6. รหัสข้อผิดพลาดและการแก้ปัญหา
อ่านทั้งรหัส HTTP, status, message และสถานะธุรกรรมภายใน data ห้ามใช้ตาราง status เดียวกับทุกบริการ เช่น Topup ใช้สตริง ส่วนรหัสบัตรใช้ตัวเลข
| รหัสข้อผิดพลาดทั่วไป | สาเหตุ |
|---|---|
| 100 | ไม่พบสินค้าหรือคำขอแลกบัตรผิดพลาด; อ่าน message |
| 101 | ไม่พบกระเป๋าอิเล็กทรอนิกส์ |
| 102 | ยอดกระเป๋าไม่พอ |
| 103 | ข้อมูลไม่ครบ |
| 104 | ข้อมูลที่ส่งไม่ถูกต้อง |
| 108 | Merchant ไม่มีอยู่ ถูกปิด หรือไม่ถูกต้อง |
| 109 | request_id มีอยู่แล้ว |
| 110 | คำขอไม่ถูกต้อง ประเภท Merchant ไม่ตรง หรือไม่พบคำสั่งซื้อ |
| 111 | wallet_number ไม่ตรงกันใน API รหัสบัตร |
| 116 | ลายเซ็นไม่ถูกต้อง |
| 117 | command ขาด/ผิด |
| 118 | สินค้าหมดหรือยังไม่ให้บริการ |
| 114 | IP ไม่ได้รับอนุญาต |
| 129 | ไม่พบคำสั่งซื้อรหัสบัตร |
| 130 | ข้อผิดพลาดผู้ให้บริการ |
| 125 | จำนวนไม่ถูกต้อง |
| 126 | จำนวนต้องเป็นจำนวนเต็มบวก |
| กรณี | แนวทางดำเนินการ |
|---|---|
| HTTP 400 | ตรวจสอบข้อมูล ฟิลด์ลูกค้า ขีดจำกัดจำนวนเงิน และตัวเลือกสินค้า |
| HTTP 409 / request_id ซ้ำ | ตรวจสอบคำสั่งซื้อเดิม ห้ามส่ง payload อื่นด้วยรหัสที่ใช้แล้ว |
| HTTP 503 / timeout | คงรหัสคำขอไว้ และกระทบยอดก่อนลองใหม่ สถานะการส่งให้ผู้ให้บริการอาจยังไม่ชัดเจน |
| มี API success แต่ยังเป็น pending | ติดตามสถานะ/callback ต่อ; ธุรกรรมยังไม่เสร็จ |
| Sai sign | ตรวจสอบคีย์บริการ ลำดับการต่อข้อความ request_id และ UTF-8 ให้ถูกต้อง |
| ราคาเปลี่ยนจากที่ดูก่อนหน้า | ดึงหมวดหมู่ตาม Merchant ใหม่ โดยกำหนดราคา ณ เวลารับคำสั่งซื้อ |
ควรเว้นระยะค้นหาอย่างน้อยไม่กี่วินาที และเพิ่มเวลารอเมื่อยังไม่มีผลลัพธ์ เซิร์ฟเวอร์ของคุณต้องสร้างและบันทึก request_id ก่อนส่ง อย่าใช้การซื้อจริงทดสอบการเชื่อมต่อ; productlist/getbalance/getfee เป็นการตรวจสอบแบบอ่านอย่างเดียว
7. ตัวอย่างโค้ด
ตัวอย่าง Python เรียก getbalance อย่างปลอดภัยเพื่อตรวจการเชื่อมต่อ (ติดตั้ง requests ด้วย pip install requests) ใช้ตัวแปรสภาพแวดล้อมสำหรับข้อมูลส่วนตัว ห้ามเขียนคีย์ในซอร์สโค้ด
import os, hashlib, requests
BASE = "https://toctoico.com"
partner_id = os.environ["GACHTHE_PARTNER_ID"]
partner_key = os.environ["GACHTHE_PARTNER_KEY"]
def recharge(command, **fields):
request_id = str(fields.get("request_id", "")) if command in ("topup", "getstatus") else ""
sign = hashlib.md5((partner_key + partner_id + command + request_id).encode()).hexdigest()
payload = {**fields, "partner_id": partner_id, "command": command, "sign": sign}
response = requests.post(BASE + "/api/rechargews", json=payload, timeout=20)
result = response.json()
response.raise_for_status()
return result
print(recharge("getbalance"))
# Lưu request_id trước khi gọi topup. Sau đó lưu order_code trả về.
# Khi timeout: tra cứu/đối soát, không tạo request_id mới để mua lại.PHP: ลายเซ็นและการส่ง JSON
<?php
$partnerId = getenv('GACHTHE_PARTNER_ID');
$partnerKey = getenv('GACHTHE_PARTNER_KEY');
$command = 'getbalance';
$payload = ['partner_id' => $partnerId, 'command' => $command,
'sign' => md5($partnerKey . $partnerId . $command)];
$ch = curl_init('https://toctoico.com/api/rechargews');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_TIMEOUT => 20]);
$body = curl_exec($ch);
if ($body === false) { throw new RuntimeException('Chưa xác nhận được phản hồi API'); }
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
// Kiểm tra $status, $result['status'] và trạng thái đơn trong data nếu có.cURL
curl --request POST 'https://toctoico.com/api/rechargews' \
--header 'Content-Type: application/json' \
--data '{"partner_id":"YOUR_PARTNER_ID","command":"getbalance","sign":"YOUR_COMPUTED_MD5"}' การแลกบัตรใช้สูตร charging/check แยกตามตารางลายเซ็น สูตรเดิม MD5(partner_key + code + serial) ยังใช้ได้ สำหรับซื้อรหัสบัตร ให้ใช้กฎต่อสตริงเดียวกับตารางลายเซ็นและ endpoint /api/cardws
8. ค้นหาซีเรียล
POST /api/checkcard. ใช้ได้เมื่อผู้ดูแลเปิดฟีเจอร์และอนุญาต Merchant แล้วเท่านั้น ความสามารถในการค้นหา วงเงิน และข้อมูลที่คืนขึ้นอยู่กับการตั้งค่าที่ได้รับอนุมัติ อย่าสรุปว่าบัตรยังไม่ได้ใช้จากการตอบกลับที่มีข้อมูลไม่ครบ
| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| partner_id | จำเป็น | Merchant มีสิทธิ์ |
| telco | Khi charging | รหัสประเภทบัตร |
| serial | จำเป็น | สตริงซีเรียล |
| sign | จำเป็น | MD5(partner_key + serial) |
{
"partner_id": "YOUR_PARTNER_ID",
"telco": "VIETTEL",
"serial": "YOUR_CARD_SERIAL",
"sign": "MD5_PARTNER_KEY_SERIAL"
}ตรวจสอบ status, message และข้อมูลจริงที่คืนมา อย่าถือว่านี่เป็น API เติมบัตรหรือตรวจสอบ PIN ห้ามเปิดสิทธิ์เองหรือค้นหาเป็นจำนวนมาก
Tool Rejoin · API สำหรับตัวแทนจำหน่าย
1. ขั้นตอนการเชื่อมต่อ
API นี้ใช้ขาย ต่ออายุ และรีเซ็ตคีย์ VIP ของ Tool Rejoin ด้วยกระเป๋า VND บน TOCTOICO เป็นโปรโตคอล REST เฉพาะของ Rejoin ห้ามส่ง command หรือลายเซ็น MD5 ของบริการบัตร/Topup ทั้งสามมายัง endpoint นี้
- เข้าสู่ระบบเว็บ → เชื่อมต่อ API, สร้าง Merchant ประเภท Tool Rejoin, เปิดใช้งานและเก็บ Partner ID/Partner Key ไว้ในแบ็กเอนด์ของคุณ
- เรียก GET /plans เพื่อดูแพ็กเกจที่เปิดขาย ราคา และสถานะ service_ready อ่าน GET /balance เพื่อตรวจสอบยอดกระเป๋า
- บันทึกรหัสคำสั่งซื้อฝั่งคุณ plan_id และ quantity ก่อนส่ง POST /keys/purchase และใช้รหัสนั้นเป็น Idempotency-Key
- อ่านคีย์จากผลตอบกลับสำเร็จ หรือเรียก GET /orders/{request_id} อีกครั้งหากขาดคำตอบ แสดงคีย์ให้ผู้ซื้ออย่างปลอดภัย
- ต่ออายุหรือรีเซ็ตด้วย UUID ของคีย์ที่เป็นของบัญชี Merchant โดยแต่ละการดำเนินการมี Idempotency-Key แยกกัน
คีย์ VIP เริ่มนับอายุเมื่อเปิดใช้งานบนอุปกรณ์อย่างถูกต้องครั้งแรก ราคาดึงจากฐานข้อมูลเมื่อประมวลผลธุรกรรมใหม่ ไม่ใช้ราคาที่คุณส่งมาเอง ราคาและจำนวนวันในตัวอย่างเป็นเพียงการสาธิต ใช้ GET /plans เพื่ออ่านแพ็กเกจจริง คีย์ Free ออกผ่าน Discord ไม่สามารถซื้อหรือรีเซ็ตผ่าน API ตัวแทนนี้
2. การยืนยันตัวตน สิทธิ์ และรูปแบบข้อมูล
Authorization: Bearer YOUR_PARTNER_KEY
X-Partner-ID: YOUR_PARTNER_ID
Accept: application/json
Content-Type: application/json
Idempotency-Key: shop_order_20261030_000001| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| Authorization | ทุกคำขอ | Bearer + Partner Key ของ Merchant บนเว็บนี้ |
| X-Partner-ID | ทุกคำขอ | Partner ID ที่ตรงกัน; อย่าส่งข้อมูลยืนยันตัวตนใน URL |
| Content-Type | ทุก POST | application/json; reset ยังคงส่ง JSON object {} |
| Idempotency-Key | POST ซื้อ ต่ออายุ รีเซ็ต | อักขระ 8–100 ตัว ได้แก่ A–Z, a–z, 0–9, _ . : -; รหัสคงที่สำหรับหนึ่งการดำเนินการ |
| Accept | แนะนำ | application/json |
Merchant ต้องเปิดใช้งาน เป็นประเภท Tool Rejoin หรือ All และเจ้าของบัญชีต้องไม่ถูกระงับ หากตั้งค่า IP ไว้ คำขอต้องมาจาก IP สาธารณะของเซิร์ฟเวอร์ที่อนุญาต ห้ามเพิ่ม X-Forwarded-For หรือส่วนหัว IP เองเพื่อข้ามขีดจำกัด เก็บ Partner Key ไว้ในแบ็กเอนด์เท่านั้น ห้ามใส่ใน JavaScript ฝั่งหน้าเว็บ ลิงก์ หรือ log
คีย์ถูกจำกัดตามเจ้าของกระเป๋า แม้บัญชีนั้นมีสิทธิ์ผู้ดูแลบนเว็บ ก็จัดการได้เฉพาะคีย์ VIP ของเจ้าของกระเป๋ารายนั้นที่ซื้อผ่านเว็บหรือ API รายการคำสั่งซื้อ API เป็นของ Merchant ที่เรียกเท่านั้น Idempotency-Key มีขอบเขตแยกต่อ Merchant และใช้ร่วมกับทุกประเภทการดำเนินการของ Merchant นั้น ห้ามนำรหัสซื้อกลับมาใช้ต่ออายุ/รีเซ็ต Merchant ต่างกันมีขอบเขตรหัสคำขอแยกกัน แม้เป็นเจ้าของกระเป๋าเดียวกัน
คำตอบใช้ success ชนิด boolean, data คือผลลัพธ์ ข้อผิดพลาดมี success=false, message, code และ data=null. อย่าแปลค่า status ตามตารางตัวเลขของ API บัตร จำนวนเงินเป็นจำนวนเต็ม VND และเวลาเป็น ISO 8601 UTC หน้าแอดมินอาจแสดงเป็น UTC+7
{
"success": true,
"data": {
"balance": 500000,
"currency": "VND",
"discount_percent": 0,
"discount_bps": 0
}
}3. รายการ endpoint
| Method / path | วัตถุประสงค์ | ข้อมูลเข้า |
|---|---|---|
| GET /api/rejoin/v1/balance | ยอดกระเป๋าร่วมของบัญชี Merchant | ไม่มี body |
| GET /api/rejoin/v1/plans | แพ็กเกจที่เปิดขาย ราคาปัจจุบัน และ service_ready | ไม่มี body |
| GET /api/rejoin/v1/keys | รายการคีย์ VIP ของเจ้าของกระเป๋า โดยซ่อนคีย์ | page, per_page, status, q |
| GET /api/rejoin/v1/keys/{uuid} | รายละเอียดคีย์ VIP ของเจ้าของกระเป๋า รวมคีย์เต็ม | UUID จาก license_ids หรือ keys.id |
| POST /api/rejoin/v1/keys/purchase | หักยอดกระเป๋าและออกคีย์ภายในธุรกรรมเดียวกัน | JSON: plan_id, quantity |
| POST /api/rejoin/v1/keys/{uuid}/renew | หักยอดกระเป๋าและต่ออายุคีย์เดิม | JSON: plan_id |
| POST /api/rejoin/v1/keys/{uuid}/reset | รีเซ็ตการผูกอุปกรณ์ของคีย์ VIP | JSON: {} |
| GET /api/rejoin/v1/orders | ประวัติการดำเนินการของ Merchant ปัจจุบัน โดยซ่อนคีย์ | page, per_page, request_id, operation |
| GET /api/rejoin/v1/orders/{request_id} | ตรวจสอบการดำเนินการให้ตรงกับ Idempotency-Key ที่ส่งไป | รหัสคำขอเดิม รวมคีย์เต็มหากมี |
| GET /api/rejoin/v1/users/{user_id}/discount | เฉพาะผู้ดูแล: ดูส่วนลดซื้อคีย์ของบัญชี | ID บัญชีเว็บ ไม่ใช่ Partner ID |
| POST /api/rejoin/v1/users/{user_id}/discount | เฉพาะผู้ดูแล: ตั้งส่วนลดซื้อคีย์ Rejoin | JSON: discount_percent; ไม่หักเงินหรือออกคีย์ |
4. ยอดคงเหลือและรายการแพ็กเกจ
GET /balance คืนค่า balance, currency=VND และ discount_percent, discount_bps ของเจ้าของบัญชี Merchant ซึ่งเป็นกระเป๋าเดียวกับธุรกรรมบนเว็บ ห้ามหักเงินจากกระเป๋าอื่นเพิ่มสำหรับคำสั่งซื้อเดียวกัน หากยังไม่ได้ออกแบบการกระทบยอด
{
"success": true,
"data": {
"balance": 500000,
"currency": "VND",
"discount_percent": 0,
"discount_bps": 0
}
}GET /plans คืนค่า data.items, currency, service_ready และส่วนลดซื้อคีย์ของบัญชีเดียวกัน ในแต่ละแพ็กเกจ: base_price คือราคาปกติ, price คือราคาต่อหน่วยสำหรับซื้อใหม่หลังส่วนลด, renew_price คือราคาต่ออายุที่ใช้ส่วนลด Rejoin เดียวกันของบัญชีแล้ว discount_bps คือเปอร์เซ็นต์คูณ 100 (2%=200 bps) ส่งเฉพาะ plan_id ของแพ็กเกจที่เปิดขาย service_ready=false หมายถึงบริการ VIP ยังไม่พร้อม จึงยังถือว่าเป็นการยืนยันว่าสามารถขายคีย์ไม่ได้
{
"success": true,
"data": {
"items": [
{
"id": 1,
"name": "VIP 7 ngày",
"days": 7,
"base_price": 50000,
"price": 50000,
"renew_price": 50000,
"discount_percent": 0,
"discount_bps": 0,
"reset_limit": 1,
"featured": 1
}
],
"currency": "VND",
"service_ready": true,
"discount_percent": 0,
"discount_bps": 0
}
}ส่วนลดใช้กับการซื้อใหม่และต่ออายุคีย์ VIP Tool Rejoin ทั้งเว็บและ API ของเจ้าของบัญชีเดียวกัน ไม่ใช้กับการแลกบัตร ซื้อรหัสบัตร หรือเติม Topup ตัวอย่างราคาปกติ 50.000₫ ลด 2%: price=49.000₫ ซื้อ 3 คีย์หัก 147.000₫ ราคาต่อหน่วยปัดแบบ HALF_UP เป็นจำนวนเงินเต็มก่อนคูณจำนวน (เศษตั้งแต่ 0,5₫ ปัดขึ้น) ห้ามหักส่วนลดจาก price ซ้ำ ราคาและส่วนลดจะถูกกำหนดเมื่อซื้อใหม่หรือต่ออายุ การเปลี่ยนแปลงภายหลังไม่แก้คำสั่งซื้อเดิม
ผู้ดูแล: ตั้งส่วนลดรายบัญชี
ใช้ Merchant ประเภท Tool Rejoin หรือ All ของบัญชี ผู้ดูแลที่ใช้งานอยู่ เป็นเจ้าของ API อ่านสิทธิ์ผู้ดูแลจากฐานข้อมูลอีกครั้งภายใน transaction ตัวแทนทั่วไปไม่มีสิทธิ์ดูหรือแก้ไข endpoint นี้ แม้เป็นข้อมูลของตนเอง ห้ามส่ง role, owner_id หรือสิทธิ์ admin ใน body
| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| user_id | ในเส้นทาง | ID จำนวนเต็มของบัญชีเว็บที่ต้องการตั้งค่า |
| discount_percent | ต้องใช้ POST | ค่าระหว่าง 0–100 ทศนิยมไม่เกิน 2 ตำแหน่ง ใช้จุดทศนิยม ใส่ 0 เพื่อยกเลิกส่วนลด |
{
"discount_percent": 2
}{
"success": true,
"data": {
"user_id": 123,
"discount_percent": 2,
"discount_bps": 200
}
}GET และ POST ใช้เส้นทางเดียวกัน /api/rejoin/v1/users/123/discount. POST รับเฉพาะ discount_percent เพื่อตั้งค่าแบบสัมบูรณ์และบันทึกประวัติผู้ดูแล ไม่สะสมเปอร์เซ็นต์ ไม่แก้คำสั่งซื้อเดิม ไม่สร้างรายการใน /orders และไม่ต้องใช้ Idempotency-Key การส่งค่าเดิมซ้ำคงส่วนลดเดิมไว้ หากผู้ดูแลเปลี่ยนส่วนลดไปแล้ว POST ใหม่จะยังตั้งค่าตามค่าที่ส่ง ข้อผิดพลาด: 403 permission_denied เมื่อไม่ใช่ผู้ดูแล, 404 not_found เมื่อไม่พบบัญชี, 400 invalid_request เมื่อค่าหรือฟิลด์ไม่ถูกต้อง กฎการยืนยันตัวตน Merchant, IP และขีดจำกัดคำขอยังคงใช้
curl --silent --show-error --include --request POST 'https://toctoico.com/api/rejoin/v1/users/123/discount' \
--header 'Authorization: Bearer YOUR_ADMIN_PARTNER_KEY' \
--header 'X-Partner-ID: YOUR_ADMIN_PARTNER_ID' \
--header 'Content-Type: application/json' \
--data '{"discount_percent":2}' 5. ซื้อคีย์ VIP
POST /api/rejoin/v1/keys/purchase
| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| plan_id | จำเป็น | ID จำนวนเต็มของแพ็กเกจที่เปิดขายจาก GET /plans |
| quantity | ไม่บังคับ ค่าเริ่มต้น 1 | จำนวนเต็มตั้งแต่ 1 ถึง 50; ยอดชำระรวม = ราคาซื้อใหม่ต่อหน่วยหลังลด × จำนวน |
{
"plan_id": 1,
"quantity": 1
}ห้ามส่ง price, discount_percent, discount_bps, charged, balance, duration_days หรือสิทธิ์ VIP เพื่อกำหนดราคา/สิทธิ์เอง เมื่อสำเร็จ ระบบจะหักเงินและออกคีย์ใน transaction เดียวกัน หากล้มเหลวจะไม่ออกคีย์หรือหักเงินค้างบางส่วน เก็บ request_id, order_code, license_ids และ charged ไว้เพื่อกระทบยอด
{
"success": true,
"data": {
"request_id": "shop_order_20261030_000001",
"operation": "purchase",
"status": "completed",
"order_code": "RJ_EXAMPLE_ORDER",
"license_ids": [
"11111111-2222-4333-8444-555555555555"
],
"charged": 50000,
"balance": 450000,
"keys": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO0001-DEMO0002-DEMO0003-DEMO0004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
],
"created_at": "2026-10-30T02:00:00+00:00",
"pricing": {
"base_unit_price": 50000,
"unit_price": 50000,
"quantity": 1,
"discount_percent": 0,
"discount_bps": 0
}
}
}| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| request_id | ได้รับ | Idempotency-Key ที่คุณส่ง |
| operation, status | ได้รับ | purchase และ completed เมื่อดำเนินการเสร็จแล้ว |
| order_code | ได้รับ | รหัสธุรกรรมกระเป๋าในระบบ ใช้เทียบกับหน้าแอดมิน |
| license_ids | ได้รับ | รายการ UUID ของคีย์ ไม่ใช่สตริงคีย์ RJ-... |
| charged, balance | ได้รับ | ยอดที่หักทั้งหมดและยอดคงเหลือหลังดำเนินการ เป็น VND จำนวนเต็ม |
| pricing | ได้รับ | ราคาและส่วนลดที่กำหนดไว้ของการซื้อ/ต่ออายุ เป็น null เมื่อรีเซ็ตหรือเป็นคำสั่งซื้อเก่าที่ยังไม่มีหลักฐาน snapshot |
| keys | ได้รับ | คีย์เต็มและสิทธิ์ที่ออกให้ เก็บอย่างปลอดภัยและไม่บันทึกใน log |
| expires_at | Trong key | null เมื่อยังไม่เปิดใช้ เริ่มนับอายุจากการเปิดใช้ที่ถูกต้องครั้งแรก |
6. รายการและรายละเอียดคีย์
GET /api/rejoin/v1/keys?page=1&per_page=20. page เริ่มจาก 1 และ per_page อยู่ในช่วง 1–100 ค่า status อาจเป็น inactive, active, expired หรือ revoked และ q ใช้ค้นหา การแบ่งหน้าคืนค่า page, per_page, total และ pages รายการจะซ่อนคีย์ ห้ามนำค่าที่ถูกซ่อนไปกรอกในเครื่องมือ
{
"success": true,
"data": {
"items": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO••••••••00004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
],
"page": 1,
"per_page": 20,
"total": 1,
"pages": 1
}
}เรียก GET /api/rejoin/v1/keys/11111111-2222-4333-8444-555555555555 เพื่อรับ data รายละเอียดที่มีคีย์เต็ม คืนเฉพาะคีย์ VIP ของเจ้าของบัญชี UUID ผิดหรือคีย์ที่ไม่มีสิทธิ์จะถูกปฏิเสธ status=inactive ยังไม่เปิดใช้; active ยังมีอายุ; expired หมดอายุ; revoked ถูกเพิกถอน
{
"success": true,
"data": {
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO0001-DEMO0002-DEMO0003-DEMO0004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
}| พารามิเตอร์ | คำขอ | ความหมาย / ค่า |
|---|---|---|
| id, owner_id, plan_id | Trong key | UUID คีย์ ID เจ้าของบัญชี และ ID แพ็กเกจ |
| kind, max_tabs | Trong key | VIP สูงสุด 4 แท็บ |
| key | Trong key | ซ่อนในรายการ แสดงเต็มในรายละเอียด/การซื้อ/การค้นหาคำสั่งซื้อเฉพาะรายการ |
| device, device_id | Trong key | ชื่ออุปกรณ์และแฮชรหัสเครื่อง เป็นสตริงว่างเมื่อยังไม่ผูกเครื่องหรือรีเซ็ตแล้ว |
| generation | Trong key | เวอร์ชันการผูกอุปกรณ์; เพิ่มเมื่อรีเซ็ตเพื่อยกเลิกบริบทการออกเซสชันเดิม |
| activated_at, expires_at, last_seen | Trong key | ISO 8601 UTC หรือ null; เวลาที่เปิดใช้งานครั้งแรก วันหมดอายุ และการยืนยันตัวตนล่าสุด |
| duration_days, resets_remaining | Trong key | จำนวนวันที่ได้รับและสิทธิ์รีเซ็ตคงเหลือ |
| q | กรองรายการคีย์ | สูงสุด 100 อักขระ ค้นหาด้วย UUID คีย์ที่ปกปิด หรือชื่ออุปกรณ์ ห้ามค้นหาด้วยคีย์เต็ม |
7. ต่ออายุและรีเซ็ตเพื่อย้ายเครื่อง
POST /api/rejoin/v1/keys/{uuid}/renew พร้อม body {"plan_id":1}, และ Idempotency-Key ใหม่ การต่ออายุใช้ renew_price ซึ่งรวมส่วนลด Rejoin ของเจ้าของบัญชี ณ เวลาต่ออายุแล้ว ห้ามหักส่วนลดซ้ำ ฟิลด์ pricing เก็บราคาและอัตราที่ตกลงไว้สำหรับคำสั่งต่ออายุ คีย์ที่เปิดใช้งานแล้ว: วันหมดอายุใหม่ = max(วันหมดอายุเดิม, เวลาปัจจุบัน) + จำนวนวันของแพ็กเกจ คีย์ที่ยังไม่เปิดใช้งาน: เพิ่มจำนวนวันที่รอเปิดใช้งาน โดยยังไม่เริ่มนับอายุ เพิ่มสิทธิ์รีเซ็ตตามแพ็กเกจ และคงคีย์เดิมกับการผูกอุปกรณ์ไว้
POST /api/rejoin/v1/keys/{uuid}/reset พร้อม body {}, และ Idempotency-Key ใหม่ การรีเซ็ตใช้สิทธิ์หนึ่งครั้ง ล้างการผูกอุปกรณ์บนเซิร์ฟเวอร์ เพิ่ม generation และคงวันหมดอายุเดิม การรีเซ็ตคืนค่า charged=0, order_code=null หากสิทธิ์หมดจะคืนข้อผิดพลาด ไม่รองรับ force reset สำหรับตัวแทน คีย์ Free หรือคีย์ที่ถูกเพิกถอน
รีเซ็ตไม่ลบไฟล์บนโทรศัพท์ ไม่ส่ง push เพื่อตัดทันที และไม่เพิ่มวันใช้งานเอง ปิดทูลบนเครื่องเก่าก่อนรีเซ็ตเพื่อไม่ให้เครื่องเก่าแย่งผูกคีย์อีก จากนั้นเปิดทูลบนเครื่องใหม่แล้วใส่คีย์ การเพิกถอนเซสชันเก่าขึ้นอยู่กับรอบยืนยันตัวตนของทูล
{
"success": true,
"data": {
"request_id": "shop_reset_20261030_000001",
"operation": "reset",
"status": "completed",
"order_code": null,
"license_ids": [
"11111111-2222-4333-8444-555555555555"
],
"charged": 0,
"balance": 450000,
"keys": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO0001-DEMO0002-DEMO0003-DEMO0004",
"status": "active",
"device": "",
"expires_at": "2026-11-06T02:00:00+00:00",
"duration_days": 7,
"resets_remaining": 0,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": "2026-10-30T02:00:00+00:00",
"last_seen": "2026-10-30T02:00:00+00:00",
"device_id": "",
"generation": 2
}
],
"created_at": "2026-10-30T02:00:00+00:00",
"pricing": null
}
}ทูลขอยืนยันตัวตนใหม่ทุก 120 วินาที และ lease มีอายุสูงสุด 300 วินาที นี่ไม่ใช่การรับประกันว่าจะตัดเครื่องเก่าทันทีหรือใช้งาน offline ได้ครบ 300 วินาทีหลังทุกปัญหาเครือข่าย; ปิดเซสชันเก่าก่อนย้ายเครื่อง
8. ค้นหาคำสั่งซื้อและลองใหม่อย่างปลอดภัย
GET /api/rejoin/v1/orders/{request_id} ใช้ Idempotency-Key ให้ตรงกับการซื้อ/ต่ออายุ/รีเซ็ต ห้ามใช้ order_code หรือสตริงคีย์แทนในเส้นทางนี้ ส่ง data โครงสร้างเดียวกับการดำเนินการที่สำเร็จ รวมคีย์เต็มที่มีสิทธิ์เข้าถึง หาก Merchant นี้ไม่เคยบันทึกรหัสดังกล่าว API จะตอบ 404
GET /api/rejoin/v1/orders?page=1&per_page=20&operation=purchase ตอบกลับ data.items, page, per_page, total, pages โดย operation รับ purchase, renew, reset; request_id ใช้กรองรหัสที่ตรงกันได้ คีย์ในรายการคำสั่งซื้อถูกปกปิด API Rejoin ประมวลผลธุรกรรมทันที ไม่ใช้ callback ของบัตร/Topup
- บันทึกรหัสคำขอ + payload ก่อนเรียก API รวมถึงกรณีเรียกจากคิวงาน
- Timeout หรือการเชื่อมต่อขาด: อย่าสรุปว่าล้มเหลว อย่าคืนเงินหรือส่งรายการใหม่ด้วย ID อื่น ตรวจ GET /orders/{request_id}
- หากพบคำสั่งซื้อ completed ให้ใช้ผลลัพธ์เดิม อย่าซื้อซ้ำ หากได้ 404 สามารถส่ง POST เดิมด้วย Idempotency-Key และ payload เดิมอีกครั้ง
- Retry ด้วยรหัส/เนื้อหาเดิมจะคืนผลที่บันทึกไว้ ไม่หักเงินหรือใช้สิทธิ์รีเซ็ตเพิ่ม รหัสเดิมแต่ payload หรือประเภทการดำเนินการต่างกันจะคืน 409
- 429/503: รอตาม Retry-After หากมี ค้นหาคำสั่งซื้อเดิมแล้วลองใหม่แบบจำกัดครั้ง โดยคงรหัสและเนื้อหาเดิม หากสถานะยังไม่ชัดเจน ให้คงคำสั่งซื้อไว้เพื่อรอกระทบยอด
ห้ามใช้การหมดเวลารอของเบราว์เซอร์หรือ HTTP error เป็นหลักฐานว่ากระเป๋ายังไม่ถูกหักเงิน การเปลี่ยนราคาแพ็กเกจหรือส่วนลดบัญชีหลังคำสั่งซื้อเสร็จแล้ว ไม่เปลี่ยน charged และ pricing ที่กำหนดไว้ของคำสั่งซื้อนั้น
ผลการค้นหาคำสั่งซื้อ/retry คง request_id, order_code, charged, pricing, license_ids และยอดเงินหลังการดำเนินการนั้น pricing มี base_unit_price (ราคาปกติ), unit_price (ราคาต่อหน่วยหลังส่วนลด), quantity, discount_percent และ discount_bps จาก snapshot ของคำสั่งซื้อ ไม่อ่านส่วนลดปัจจุบัน คำสั่งซื้อเก่าที่ไม่มี snapshot จะคืน pricing=null แทนการคาดเดาส่วนลด ฟิลด์ keys แสดงสถานะคีย์ปัจจุบัน ซึ่งอาจถูกต่ออายุ รีเซ็ต หรือผูกอุปกรณ์หลังธุรกรรมเดิม หากต้องการยอดปัจจุบัน ให้เรียก GET /balance การ retry รีเซ็ตหลังคีย์ผูกกับเครื่องใหม่แล้วยังคืนผลสำเร็จของการดำเนินการเดิม โดยไม่รีเซ็ตเพิ่มอีกครั้ง
{
"success": true,
"data": {
"items": [
{
"request_id": "shop_order_20261030_000001",
"operation": "purchase",
"status": "completed",
"order_code": "RJ_EXAMPLE_ORDER",
"license_ids": [
"11111111-2222-4333-8444-555555555555"
],
"charged": 50000,
"balance": 450000,
"keys": [
{
"id": "11111111-2222-4333-8444-555555555555",
"owner_id": 123,
"plan_id": 1,
"kind": "vip",
"key": "RJ-DEMO••••••••00004",
"status": "inactive",
"device": "",
"expires_at": null,
"duration_days": 7,
"resets_remaining": 1,
"created_at": "2026-10-30T02:00:00+00:00",
"max_tabs": 4,
"activated_at": null,
"last_seen": null,
"device_id": "",
"generation": 1
}
],
"created_at": "2026-10-30T02:00:00+00:00",
"pricing": {
"base_unit_price": 50000,
"unit_price": 50000,
"quantity": 1,
"discount_percent": 0,
"discount_bps": 0
}
}
],
"page": 1,
"per_page": 20,
"total": 1,
"pages": 1
}
}9. HTTP และข้อผิดพลาด
| HTTP | ความหมาย / วิธีดำเนินการ |
|---|---|
| 400 | JSON พารามิเตอร์ หรือ Idempotency-Key ขาด/ผิด; แก้ข้อมูลก่อนสร้างการดำเนินการใหม่ |
| 401 | Partner ID หรือ Bearer Partner Key ขาด/ผิด หรือ Merchant ถูกปิด |
| 402 | ยอดกระเป๋าไม่พอ กรุณาเติมเงินและตรวจยอดอีกครั้ง |
| 403 | ประเภท Merchant ไม่ตรง IP ไม่ได้รับอนุญาต บัญชีถูกล็อก หรือไม่ได้รับสิทธิ์ดำเนินการ |
| 404 | ไม่มีแพ็กเกจ/คีย์/คำสั่งซื้อที่อยู่ในสิทธิ์ ดู code และ message เพื่อแยกสาเหตุ |
| 405 | HTTP method ไม่ถูกต้อง; ใช้ GET/POST ตามตาราง endpoint |
| 409 | Idempotency-Key ขัดแย้งหรือสิทธิ์รีเซ็ตหมด ห้ามเปลี่ยนรหัสเพื่อหลบข้อผิดพลาด |
| 429 | เกินขีดจำกัด request; ลดความถี่ รอ และตรวจรายการเดิม |
| 503 | VIP หรือบริการยังไม่พร้อม กรุณาเก็บรหัสคำขอเพื่อกระทบยอด |
| code ที่พบบ่อย | HTTP |
|---|---|
| authentication_required, authentication_failed, merchant_disabled | 401 |
| module_forbidden, account_denied, ip_forbidden, permission_denied, purchase_limit | 403 |
| not_found | 404 |
| method_not_allowed | 405 |
| invalid_request | 400 |
| idempotency_conflict, reset_limit | 409 |
| insufficient_balance | 402 |
| rate_limited | 429 |
| vip_unavailable, service_unavailable, discount_unavailable | 503 |
{
"success": false,
"data": null,
"code": "insufficient_balance",
"message": "Số dư không đủ. Vui lòng nạp thêm tiền."
}ขีดจำกัดปัจจุบัน: 100 คำขอ/นาทีต่อ Merchant และ 120 คำขอ/นาทีต่อ IP สำหรับ API Rejoin โดยแต่ละคำขอนับในทั้งสองขีดจำกัด HTTP 429 ต้องรออย่างน้อยจนถึงรอบนาทีถัดไป ตรวจสอบทั้ง HTTP และ success/code/message ห้ามใช้รหัสข้อผิดพลาดตัวเลขของ chargingws/cardws กับ Rejoin ห้ามบันทึก Authorization, Partner Key หรือคีย์ VIP เต็มใน log สำหรับช่วยเหลือ
วงเงินซื้อของบัญชี/กลุ่มยังใช้กับ API ได้แก่ มูลค่าต่อธุรกรรม ยอดซื้อรวมต่อวัน และวงเงินสำหรับบัญชีที่ยังไม่เปิดการยืนยันสองขั้นตอน หากเกินวงเงินจะคืน HTTP 403, code=purchase_limit ไม่ใช่การขอให้กรอกรหัส 2FA ใน API ตรวจสอบการตั้งค่าบัญชี/วงเงินก่อนลองใหม่ การดำเนินการที่ไม่ได้รับอนุญาต เช่น รีเซ็ตคีย์ที่ถูกเพิกถอน อาจคืน permission_denied (403)
สำเร็จคืน HTTP 200 คำตอบที่มีคีย์ครบจะส่งด้วย Cache-Control: no-store; อย่าแคชสาธารณะหรือเก็บคีย์ดิบใน localStorage หรือ log เก็บข้อมูลคีย์ไว้ใน backend และแสดงเฉพาะผู้ซื้อที่ยืนยันตัวตนแล้ว
10. ตัวอย่างโค้ดฉบับสมบูรณ์
รันจากแบ็กเอนด์ของตัวแทน โดยเปิดการตรวจสอบ HTTPS ตามค่าเริ่มต้น กรอกตัวแปรสภาพแวดล้อมด้วย Merchant บนเว็บนี้ ห้ามใช้ client API challenge/activate ของเครื่องมือแทน API ขายสินค้า ตัวอย่าง POST สำหรับซื้อ/ต่ออายุจะหักเงินจากกระเป๋าเมื่อใช้ข้อมูลจริง การรีเซ็ตจะใช้สิทธิ์รีเซ็ต
cURL: รายการสินค้า ซื้อ และตรวจสอบหลัง timeout
export REJOIN_PARTNER_ID='YOUR_PARTNER_ID'
export REJOIN_PARTNER_KEY='YOUR_PARTNER_KEY'
API='https://toctoico.com/api/rejoin/v1'
REQUEST_ID='shop_order_20261030_000001' # Persist before submitting.
curl --silent --show-error --include "$API/plans" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include --request POST "$API/keys/purchase" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $REQUEST_ID" \
--data '{"plan_id":1,"quantity":1}'
# On timeout, look up the original request ID. Never create a replacement ID.
curl --silent --show-error --include "$API/orders/$REQUEST_ID" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"cURL: ดูคีย์ ประวัติ ต่ออายุ และรีเซ็ต
ใช้ตัวแปร API/Partner จากตัวอย่างด้านบนซ้ำ แทน KEY_ID ด้วย UUID ที่ได้รับใน license_ids โดยแต่ละ POST ด้านล่างใช้รหัสการดำเนินการใหม่และบันทึกรหัสก่อนเรียก
KEY_ID='UUID_FROM_LICENSE_IDS'
curl --silent --show-error --include "$API/balance" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include "$API/keys?page=1&per_page=20&status=inactive" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include "$API/keys/$KEY_ID" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
curl --silent --show-error --include "$API/orders?page=1&per_page=20&operation=purchase" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID"
# Renewal debits the wallet using the selected plan price.
curl --silent --show-error --include --request POST "$API/keys/$KEY_ID/renew" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: shop_renew_20261030_000001" \
--data '{"plan_id":1}'
# Stop the tool on the old device first. Reset consumes one allowance.
curl --silent --show-error --include --request POST "$API/keys/$KEY_ID/reset" \
--header "Authorization: Bearer $REJOIN_PARTNER_KEY" \
--header "X-Partner-ID: $REJOIN_PARTNER_ID" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: shop_reset_20261030_000001" \
--data '{}' Python 3: ซื้อ ค้นหารายการ และ retry ด้วยรหัสเดิม
ใช้ไลบรารีมาตรฐาน ไม่ต้องติดตั้ง requests ต้องบันทึกตัวแปร REJOIN_ORDER_ID ให้ผูกกับคำสั่งซื้อของคุณก่อนส่ง
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
BASE = os.environ.get("REJOIN_API_BASE", "https://toctoico.com/api/rejoin/v1").rstrip("/")
PARTNER_ID = os.environ["REJOIN_PARTNER_ID"]
PARTNER_KEY = os.environ["REJOIN_PARTNER_KEY"]
class APIError(Exception):
def __init__(self, status, result):
if not isinstance(result, dict):
result = {"message": "HTTP error without a valid API object"}
super().__init__(result.get("message", "API request failed"))
self.status, self.result = status, result
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None # Never forward API credentials to a redirected host.
OPENER = urllib.request.build_opener(NoRedirect())
def api(method, path, payload=None, request_id=None):
headers = {"Authorization": "Bearer " + PARTNER_KEY,
"X-Partner-ID": PARTNER_ID, "Accept": "application/json"}
if request_id is not None:
headers["Idempotency-Key"] = request_id
data = None
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
try:
with OPENER.open(request, timeout=20) as response:
try:
result = json.load(response)
except (ValueError, UnicodeError):
raise urllib.error.URLError("Invalid API response; outcome not confirmed") from None
except urllib.error.HTTPError as error:
try:
result = json.load(error)
except (ValueError, UnicodeError):
result = {"message": "HTTP error without a valid JSON body"}
raise APIError(error.code, result) from None
if not isinstance(result, dict):
raise urllib.error.URLError("Invalid API response; outcome not confirmed")
if result.get("success") is not True:
raise APIError(200, result)
if not isinstance(result.get("data"), dict):
raise urllib.error.URLError("Invalid API response; outcome not confirmed")
return result["data"]
def order(request_id):
return api("GET", "/orders/" + urllib.parse.quote(request_id, safe=""))
def purchase(plan_id, quantity, request_id):
# Save request_id + payload in your database BEFORE calling this function.
payload = {"plan_id": plan_id, "quantity": quantity}
try:
return api("POST", "/keys/purchase", payload, request_id)
except APIError as error:
if error.status not in (429, 503):
raise # Never replace a rejected 409 payload with a new request ID.
time.sleep(60 if error.status == 429 else 2)
except (urllib.error.URLError, TimeoutError):
pass
# A lost response does not mean the purchase failed. Look up the SAME ID.
try:
return order(request_id)
except APIError as error:
if error.status != 404:
raise
# Only one bounded retry, with exactly the same payload and request ID.
# If this also times out, persist "needs reconciliation" and poll order().
return api("POST", "/keys/purchase", payload, request_id)
if __name__ == "__main__":
# Use a plan_id returned by GET /plans, never infer it from a display name.
result = purchase(int(os.environ["REJOIN_PLAN_ID"]), 1, os.environ["REJOIN_ORDER_ID"])
# Store/show result["keys"] only through a protected customer flow, not logs.
print(json.dumps({name: result[name] for name in
("request_id", "order_code", "charged", "license_ids")}, ensure_ascii=False))PHP 8 + cURL: ข้อตกลงเดียวกันและรหัส retry เดียวกัน
<?php
$base = rtrim(getenv('REJOIN_API_BASE') ?: 'https://toctoico.com/api/rejoin/v1', '/');
$partnerId = getenv('REJOIN_PARTNER_ID');
$partnerKey = getenv('REJOIN_PARTNER_KEY');
class RejoinApiError extends RuntimeException {
public int $httpStatus;
public function __construct(int $status, array $result) {
parent::__construct($result['message'] ?? 'API request failed');
$this->httpStatus = $status;
}
}
class RejoinTransportError extends RuntimeException {}
function rejoinApi(string $method, string $path, ?array $payload = null,
?string $requestId = null): array {
global $base, $partnerId, $partnerKey;
$headers = ['Authorization: Bearer ' . $partnerKey,
'X-Partner-ID: ' . $partnerId, 'Accept: application/json'];
if ($requestId !== null) $headers[] = 'Idempotency-Key: ' . $requestId;
$ch = curl_init($base . $path);
$options = [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20,
CURLOPT_FOLLOWLOCATION => false];
if ($payload !== null) {
$headers[] = 'Content-Type: application/json';
// Empty reset payload must be a JSON object {}, never an array [].
$options[CURLOPT_POSTFIELDS] = json_encode($payload === [] ? new stdClass() : $payload,
JSON_THROW_ON_ERROR);
}
$options[CURLOPT_HTTPHEADER] = $headers;
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false) throw new RejoinTransportError('Response not confirmed');
try {
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
if ($status < 200 || $status >= 300) {
throw new RejoinApiError($status, ['message' => 'HTTP error without valid JSON']);
}
throw new RejoinTransportError('Invalid JSON; outcome not confirmed');
}
if (!is_array($result)) {
if ($status < 200 || $status >= 300) {
throw new RejoinApiError($status, ['message' => 'HTTP error without a valid API object']);
}
throw new RejoinTransportError('Invalid API object; outcome not confirmed');
}
if ($status < 200 || $status >= 300 || ($result['success'] ?? false) !== true) {
throw new RejoinApiError($status, $result);
}
if (!is_array($result['data'] ?? null)) {
throw new RejoinTransportError('Invalid API data; outcome not confirmed');
}
return $result['data'];
}
function rejoinOrder(string $requestId): array {
return rejoinApi('GET', '/orders/' . rawurlencode($requestId));
}
function rejoinPurchase(int $planId, int $quantity, string $requestId): array {
// Persist the request ID and payload in your database before sending.
$payload = ['plan_id' => $planId, 'quantity' => $quantity];
try {
return rejoinApi('POST', '/keys/purchase', $payload, $requestId);
} catch (RejoinApiError $error) {
if (!in_array($error->httpStatus, [429, 503], true)) throw $error;
sleep($error->httpStatus === 429 ? 60 : 2);
} catch (RejoinTransportError $error) {
// Continue with order lookup; never generate a replacement ID here.
}
try {
return rejoinOrder($requestId);
} catch (RejoinApiError $error) {
if ($error->httpStatus !== 404) throw $error;
}
// One retry with the identical payload and request ID.
// If still uncertain, keep the order for reconciliation using rejoinOrder().
return rejoinApi('POST', '/keys/purchase', $payload, $requestId);
}
$requestId = getenv('REJOIN_ORDER_ID'); // Stable business order ID, saved before I/O.
$planId = (int) getenv('REJOIN_PLAN_ID'); // Read from GET /plans.
$result = rejoinPurchase($planId, 1, $requestId);
// Reset example: stop the old tool first, then persist a NEW reset request ID.
// $uuid = $result['license_ids'][0];
// $reset = rejoinApi('POST', '/keys/' . $uuid . '/reset', [], 'shop_reset_20261030_000001');
// Store/show $result['keys'] only through a protected customer flow, not logs.
$summary = array_intersect_key($result, array_flip(['request_id', 'order_code', 'charged', 'license_ids']));
echo json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);