Survey API และการฝังในใบเสร็จ

survey api receipt targeting liff

ฟีเจอร์ Survey ของ ReceiptRoller ให้ร้านค้ารวบรวมคำตอบผ่าน QR / ลิงก์ / การฝังในใบเสร็จดิจิทัล และแจกคูปองกลับได้ตามต้องการ บทความนี้ครอบคลุม consumer API สาธารณะ เพย์โหลดการฝังในใบเสร็จ และกฎการกำหนดกลุ่มเป้าหมาย (audience targeting) ที่ร้านค้าสามารถแนบเข้ากับแคมเปญได้

URL สาธารณะ

ทุกแคมเปญแบบสำรวจจะได้รับโทเคนแบบ URL-safe ความยาว 10 อักขระ หน้าแลนดิงสาธารณะคือ:

https://{host}/s/{token}

ค่า host มาจาก Survey:UniversalLinkHost ในไฟล์คอนฟิก หากไม่มีจะใช้ host ของ request แทน แอปเนทีฟจะเคลม /s/* ผ่าน Universal Link / App Link (มีการเพิ่มรายการลงใน apple-app-site-association และ assetlinks.json) ผู้ใช้ที่เปิดผ่านเบราว์เซอร์ในแอป LINE จะถูก 302 ไปยัง LIFF URL โดยต่อท้ายด้วย ?t={token} ส่วนผู้ใช้อื่น ๆ จะไปยังฟอร์มสำรองแบบ inline บนเว็บ

Consumer API

มีสามเอนด์พอยต์ ทั้งหมดอยู่ภายใต้ /api/v1/surveys เอนด์พอยต์ preview และ submit เป็น [AllowAnonymous] เพื่อให้ LIFF หรือฟอร์มสำรองบนเว็บเรียกได้โดยไม่ต้องมี bearer token เซิร์ฟเวอร์จะควบคุมแคมเปญที่ไม่อนุญาตผู้ไม่ระบุตัวตนด้วยสถานะ auth_required

GET /api/v1/surveys/campaign/{token}

คืนค่าเมทาดาทาของแคมเปญและรายการคำถามทั้งหมด ไคลเอนต์ใช้เพื่อเรนเดอร์ฟอร์มก่อนที่ผู้ใช้จะเริ่มตอบ

{
  "status": "ok",
  "campaign": {
    "campaignId": "...",
    "storeId": "...",
    "title": "...",
    "description": "...",
    "validFrom": "2026-04-01T00:00:00Z",
    "validUntil": "2026-05-31T23:59:59Z",
    "isAnonymous": true,
    "maxResponsesPerUser": 1,
    "hasReward": true
  },
  "questions": [
    { "questionId": "...", "order": 0, "type": "single_choice",
      "text": "...", "isRequired": true, "optionsJson": "[\"A\",\"B\"]" }
  ]
}

ค่า status ที่เป็นไปได้: ok, expired, inactive, unknown_token

POST /api/v1/surveys/responses

ส่งคำตอบ ผู้เรียกแบบไม่ระบุตัวตนต้องส่ง deviceId ที่คงที่ ส่วนแคมเปญที่ไม่อนุญาตผู้ไม่ระบุตัวตนต้องใช้ bearer token

POST /api/v1/surveys/responses
Content-Type: application/json

{
  "token": "...",
  "answers": [
    { "questionId": "...", "value": "A" },
    { "questionId": "...", "value": "5" },
    { "questionId": "...", "value": "OptionA,OptionC" }
  ],
  "deviceId": "anon-..."
}

การตอบกลับใช้ตัวจำแนก status ที่ไคลเอนต์นำไปแตกเงื่อนไข:

Statusความหมาย
recordedบันทึกสำเร็จ ค่า couponIssuedId จะมีค่าเมื่อแคมเปญมีรางวัลและคูปองผ่านการตรวจสอบ
response_limitผู้ใช้ส่งคำตอบครบ maxResponsesPerUser ครั้งแล้ว
expiredอยู่นอกช่วงเวลาที่ใช้งานได้
inactiveแคมเปญถูกจัดเก็บ (archived)
unknown_tokenไม่พบโทเคน
validation_errorฟิลด์ที่จำเป็นว่างหรือค่าอยู่นอกช่วง มี dict validationErrors โดยใช้ questionId เป็นคีย์
auth_requiredแคมเปญต้องการผู้ใช้ที่ล็อกอิน ไคลเอนต์ควรเปิดหน้าล็อกอิน

การเข้ารหัสคำตอบแบบเลือกได้หลายข้อ

สำหรับคำถามแบบ multi_choice ไคลเอนต์อาจส่งเป็นสตริงที่คั่นด้วยจุลภาค ("A,C") หรือสตริง JSON array ("[\"A\",\"C\"]") ก็ได้ ตัวรวบรวมข้อมูลการวิเคราะห์ (analytics aggregator) รับได้ทั้งสองแบบ

GET /api/v1/surveys/me

ต้องใช้ bearer เท่านั้น คืนค่าประวัติคำตอบของผู้เรียกเอง (response id, campaign id, store id, completed-at, coupon-issued id)

การฝังในใบเสร็จดิจิทัล

เมื่อร้านค้ามีแคมเปญที่กำลังทำงานอยู่โดยตั้ง showOnDigitalReceipt = true และกฎการกำหนดกลุ่มเป้าหมายตรงกับบริบทของใบเสร็จ transaction-receipt API จะแนบออบเจ็กต์ survey เข้าไปในเพย์โหลดของใบเสร็จ:

GET /api/v1/rx/{claimToken}

{
  "claimToken": "...",
  "storeName": "...",
  "transaction": { ... },
  "survey": {
    "campaignId": "...",
    "title": "...",
    "description": "...",
    "url": "https://{host}/s/{token}",
    "hasReward": true
  }
}

ฟิลด์ survey จะเป็น null เมื่อไม่มีแคมเปญที่ตรงกัน ไคลเอนต์เพียงซ่อนส่วนนั้นไว้ ความล้มเหลวในการค้นหาข้อมูลการฝังจะไม่ทำให้การตอบกลับใบเสร็จเสียหาย

กฎการกำหนดกลุ่มเป้าหมาย

แต่ละแคมเปญสามารถแนบเอกสาร JSON ที่กรองว่าใบเสร็จใดจะได้รับการฝัง เอกสารนี้เป็นรายการแบบ AND ที่ราบ (flat) — ใบเสร็จจะตรงก็ต่อเมื่อทุกกฎตรงกัน เอกสารที่ว่างเปล่า / ไม่มี = ตรงเสมอ

{
  "rules": [
    { "field": "total_amount",         "op": "gte",      "value": 1000 },
    { "field": "item_count",           "op": "gte",      "value": 3 },
    { "field": "category",             "op": "eq",       "value": "drinks" },
    { "field": "product_name",         "op": "contains", "value": "coffee" },
    { "field": "time_of_day",          "op": "gte",      "value": 18 },
    { "field": "day_of_week",          "op": "eq",       "value": "fri" },
    { "field": "customer_visit_count", "op": "gte",      "value": 2 }
  ]
}

ฟิลด์

Fieldแหล่งที่มาชนิด
total_amountยอดรวมของใบเสร็จdecimal
item_countจำนวนรายการสินค้าint
categoryหมวดหมู่ของรายการสินค้าใด ๆstring
product_nameชื่อสินค้าของรายการใด ๆstring
time_of_dayชั่วโมงของวันที่เกิดธุรกรรม (0-23)int
day_of_weekตัวย่อภาษาอังกฤษ 3 ตัวอักษร (mon...sun)string
customer_visit_countจำนวนครั้งการมาเยือนสะสมของผู้ใช้รายนี้ (โหลดแบบ lazy-loaded)int

ตัวดำเนินการ (Operators)

  • eq — ตรงกันแบบพอดี (สตริงหรือตัวเลข)
  • gt / gte / lt / lte — การเปรียบเทียบเชิงตัวเลข
  • contains — สตริงย่อยแบบไม่สนใจตัวพิมพ์ (case-insensitive) (สำหรับฟิลด์สตริงและอาร์เรย์ของรายการสินค้า)

ความหมายแบบ fail-closed

JSON ที่ผิดรูปแบบ ฟิลด์ที่ไม่รู้จัก ตัวดำเนินการที่ไม่รู้จัก หรือค่าที่ขาดหาย จะทำให้กฎนั้น (และการจับคู่ทั้งหมด) ล้มเหลว นี่เป็นความตั้งใจ: ยอมไม่ฝังแบบสำรวจอย่างเงียบ ๆ ดีกว่าฝังลงในใบเสร็จที่ผิด

รางวัลคูปอง

การตั้ง RewardCouponId ให้กับแคมเปญจะทำให้ SubmitResponseAsync resolve คูปอง ณ เวลาที่ส่งคำตอบ คูปองต้องอยู่ในสถานะใช้งานได้และอยู่ในช่วงเวลาที่ใช้งานได้ของตนเอง หากผ่านเกณฑ์ แถวคำตอบจะถูกประทับด้วย coupon id และ DTO ผลลัพธ์จะคืนค่านั้นเป็น couponIssuedId คูปองใน ReceiptRoller เป็นเทมเพลตระดับร้านค้า — ไม่มีโมเดลการมอบสิทธิ์แบบต่อผู้ใช้ ไคลเอนต์ฝั่งผู้บริโภคจะถือว่า id ที่คืนมาเป็นรหัสที่นำไปแลกได้

การกันตอบซ้ำต่อผู้ใช้

แต่ละแคมเปญมีการตั้งค่า maxResponsesPerUser (ค่าเริ่มต้น 1) เซอร์วิสบังคับใช้เพดานนี้โดยนับคำตอบที่อ้างอิงกับ userId (การส่งแบบล็อกอิน) หรือ deviceId (การส่งแบบไม่ระบุตัวตน) ฟอร์มสำรองบนเว็บจะสร้าง device id ที่คงที่ใน localStorage ส่วนไคลเอนต์ LIFF / เนทีฟควรส่งของตนเองมา

Universal Link / App Link

พาธ /s/* ถูกรวมอยู่ใน apple-app-site-association และ assetlinks.json ควบคู่กับพาธที่มีอยู่แล้ว ได้แก่ terminal QR (/r/*), transaction QR (/rx/*) และ check-in (/c/*) แอปเนทีฟที่เคลมพาธเหล่านั้นจะได้รับ survey URL โดยตรงจาก OS และสามารถเรียก consumer API ได้โดยไม่ต้องผ่านหน้าแลนดิงบนเว็บ

เอกสารอ้างอิงเชิงวิศวกรรม

สเปกการออกแบบฉบับเต็ม รวมถึงรายการ PR แบบเฟสต่อเฟสและสิ่งที่เลื่อนไปเวอร์ชัน v2 อยู่บน engineering wiki: Engineering / Survey Feature — Design Spec

วันที่เผยแพร่: 2569-04-25 วันที่อัปเดต: 2569-07-05