[{"data":1,"prerenderedAt":799},["ShallowReactive",2],{"page-\u002Fwebhook\u002Fkeamanan-retry":3},{"id":4,"title":5,"body":6,"description":791,"endpoint":792,"extension":793,"icon":792,"meta":794,"method":792,"nav":792,"navigation":95,"path":795,"seo":796,"stem":797,"__hash__":798},"docs\u002Fwebhook\u002Fkeamanan-retry.md","Keamanan & retry",{"type":7,"value":8,"toc":781},"minimark",[9,18,23,132,168,172,179,308,361,401,445,452,460,464,470,480,483,499,503,514,538,559,565,587,591,594,645,652,656,682,686,689,708,712,777],[10,11,12,13,17],"p",{},"Siapa pun yang tahu URL webhook Anda bisa mengirim request palsu. Karena itu, setiap request dari CSKU membawa Secret Key Anda di header ",[14,15,16],"code",{},"Authorization",". Tolak semua request yang nilainya tidak cocok.",[19,20,22],"h2",{"id":21},"format-request","Format request",[24,25,30],"pre",{"className":26,"code":27,"language":28,"meta":29,"style":29},"language-http shiki shiki-themes github-light github-dark","POST \u002Fcsku\u002Fwebhook HTTP\u002F1.1\nHost: api.toko-anda.com\nContent-Type: application\u002Fjson\nAuthorization: 3f9a1c7e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e8b0d1f3a\n\n{\"event\":\"channel.message_in\",\"timestamp\":1792041600, ...}\n","http","",[14,31,32,55,69,80,90,97],{"__ignoreMap":29},[33,34,37,41,45,48,51],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"szBVR","POST",[33,42,44],{"class":43},"sVt8B"," \u002Fcsku\u002Fwebhook ",[33,46,47],{"class":39},"HTTP",[33,49,50],{"class":43},"\u002F",[33,52,54],{"class":53},"sj4cs","1.1\n",[33,56,58,62,65],{"class":35,"line":57},2,[33,59,61],{"class":60},"s9eBZ","Host",[33,63,64],{"class":39},":",[33,66,68],{"class":67},"sZZnC"," api.toko-anda.com\n",[33,70,72,75,77],{"class":35,"line":71},3,[33,73,74],{"class":60},"Content-Type",[33,76,64],{"class":39},[33,78,79],{"class":67}," application\u002Fjson\n",[33,81,83,85,87],{"class":35,"line":82},4,[33,84,16],{"class":60},[33,86,64],{"class":39},[33,88,89],{"class":67}," 3f9a1c7e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e8b0d1f3a\n",[33,91,93],{"class":35,"line":92},5,[33,94,96],{"emptyLinePlaceholder":95},true,"\n",[33,98,100,103,106,108,111,114,117,119,122,125,129],{"class":35,"line":99},6,[33,101,102],{"class":43},"{",[33,104,105],{"class":53},"\"event\"",[33,107,64],{"class":43},[33,109,110],{"class":67},"\"channel.message_in\"",[33,112,113],{"class":43},",",[33,115,116],{"class":53},"\"timestamp\"",[33,118,64],{"class":43},[33,120,121],{"class":53},"1792041600",[33,123,124],{"class":43},", ",[33,126,128],{"class":127},"s7hpK","...",[33,130,131],{"class":43},"}\n",[133,134,135,150,156],"ul",{},[136,137,138,140,141,145,146,149],"li",{},[14,139,16],{}," berisi Secret Key Anda ",[142,143,144],"strong",{},"apa adanya",": tanpa awalan ",[14,147,148],{},"Bearer",", tanpa base64.",[136,151,152,153,155],{},"Tidak ada header signature atau timestamp terpisah. Verifikasi cukup dengan mencocokkan ",[14,154,16],{},".",[136,157,158,159,162,163,155],{},"Secret Key ada di ",[142,160,161],{},"Pengaturan → Secret Key",". Lihat ",[164,165,167],"a",{"href":166},"\u002Fapi\u002Fautentikasi","Autentikasi",[19,169,171],{"id":170},"verifikasi-authorization","Verifikasi Authorization",[10,173,174,175,178],{},"Bandingkan dengan fungsi constant-time, bukan ",[14,176,177],{},"==",", agar waktu perbandingan tidak membocorkan isi secret.",[24,180,184],{"className":181,"code":182,"language":183,"meta":29,"style":29},"language-js shiki shiki-themes github-light github-dark","import crypto from 'node:crypto'\n\nfunction isFromCsku(header, secret) {\n  const a = Buffer.from(header || '')\n  const b = Buffer.from(secret)\n  return a.length === b.length && crypto.timingSafeEqual(a, b)\n}\n","js",[14,185,186,200,204,228,256,272,303],{"__ignoreMap":29},[33,187,188,191,194,197],{"class":35,"line":36},[33,189,190],{"class":39},"import",[33,192,193],{"class":43}," crypto ",[33,195,196],{"class":39},"from",[33,198,199],{"class":67}," 'node:crypto'\n",[33,201,202],{"class":35,"line":57},[33,203,96],{"emptyLinePlaceholder":95},[33,205,206,209,213,216,220,222,225],{"class":35,"line":71},[33,207,208],{"class":39},"function",[33,210,212],{"class":211},"sScJk"," isFromCsku",[33,214,215],{"class":43},"(",[33,217,219],{"class":218},"s4XuR","header",[33,221,124],{"class":43},[33,223,224],{"class":218},"secret",[33,226,227],{"class":43},") {\n",[33,229,230,233,236,239,242,244,247,250,253],{"class":35,"line":82},[33,231,232],{"class":39},"  const",[33,234,235],{"class":53}," a",[33,237,238],{"class":39}," =",[33,240,241],{"class":43}," Buffer.",[33,243,196],{"class":211},[33,245,246],{"class":43},"(header ",[33,248,249],{"class":39},"||",[33,251,252],{"class":67}," ''",[33,254,255],{"class":43},")\n",[33,257,258,260,263,265,267,269],{"class":35,"line":92},[33,259,232],{"class":39},[33,261,262],{"class":53}," b",[33,264,238],{"class":39},[33,266,241],{"class":43},[33,268,196],{"class":211},[33,270,271],{"class":43},"(secret)\n",[33,273,274,277,280,283,286,289,291,294,297,300],{"class":35,"line":99},[33,275,276],{"class":39},"  return",[33,278,279],{"class":43}," a.",[33,281,282],{"class":53},"length",[33,284,285],{"class":39}," ===",[33,287,288],{"class":43}," b.",[33,290,282],{"class":53},[33,292,293],{"class":39}," &&",[33,295,296],{"class":43}," crypto.",[33,298,299],{"class":211},"timingSafeEqual",[33,301,302],{"class":43},"(a, b)\n",[33,304,306],{"class":35,"line":305},7,[33,307,131],{"class":43},[24,309,313],{"className":310,"code":311,"language":312,"meta":29,"style":29},"language-php shiki shiki-themes github-light github-dark","\u003C?php\n$ok = hash_equals(getenv('CSKU_SECRET'), $_SERVER['HTTP_AUTHORIZATION'] ?? '');\n","php",[14,314,315,323],{"__ignoreMap":29},[33,316,317,320],{"class":35,"line":36},[33,318,319],{"class":39},"\u003C?",[33,321,322],{"class":53},"php\n",[33,324,325,328,331,334,336,339,341,344,347,350,353,356,358],{"class":35,"line":57},[33,326,327],{"class":43},"$ok ",[33,329,330],{"class":39},"=",[33,332,333],{"class":53}," hash_equals",[33,335,215],{"class":43},[33,337,338],{"class":53},"getenv",[33,340,215],{"class":43},[33,342,343],{"class":67},"'CSKU_SECRET'",[33,345,346],{"class":43},"), $_SERVER[",[33,348,349],{"class":67},"'HTTP_AUTHORIZATION'",[33,351,352],{"class":43},"] ",[33,354,355],{"class":39},"??",[33,357,252],{"class":67},[33,359,360],{"class":43},");\n",[24,362,366],{"className":363,"code":364,"language":365,"meta":29,"style":29},"language-python shiki shiki-themes github-light github-dark","import hmac\nok = hmac.compare_digest(request.headers.get(\"Authorization\", \"\"), SECRET)\n","python",[14,367,368,375],{"__ignoreMap":29},[33,369,370,372],{"class":35,"line":36},[33,371,190],{"class":39},[33,373,374],{"class":43}," hmac\n",[33,376,377,380,382,385,388,390,393,396,399],{"class":35,"line":57},[33,378,379],{"class":43},"ok ",[33,381,330],{"class":39},[33,383,384],{"class":43}," hmac.compare_digest(request.headers.get(",[33,386,387],{"class":67},"\"Authorization\"",[33,389,124],{"class":43},[33,391,392],{"class":67},"\"\"",[33,394,395],{"class":43},"), ",[33,397,398],{"class":53},"SECRET",[33,400,255],{"class":43},[24,402,406],{"className":403,"code":404,"language":405,"meta":29,"style":29},"language-go shiki shiki-themes github-light github-dark","ok := subtle.ConstantTimeCompare([]byte(r.Header.Get(\"Authorization\")), secret) == 1\n","go",[14,407,408],{"__ignoreMap":29},[33,409,410,412,415,418,421,424,427,430,433,435,437,440,442],{"class":35,"line":36},[33,411,379],{"class":43},[33,413,414],{"class":39},":=",[33,416,417],{"class":43}," subtle.",[33,419,420],{"class":211},"ConstantTimeCompare",[33,422,423],{"class":43},"([]",[33,425,426],{"class":39},"byte",[33,428,429],{"class":43},"(r.Header.",[33,431,432],{"class":211},"Get",[33,434,215],{"class":43},[33,436,387],{"class":67},[33,438,439],{"class":43},")), secret) ",[33,441,177],{"class":39},[33,443,444],{"class":53}," 1\n",[10,446,447,448,451],{},"Jika tidak cocok, balas ",[14,449,450],{},"{\"success\": false}"," dan jangan proses payload.",[453,454,457],"callout",{"title":455,"type":456},"Simpan secret di server","danger",[10,458,459],{},"Secret Key yang sama dipakai untuk memanggil Public API. Simpan hanya di server (environment variable atau secret manager). Jangan taruh di kode frontend, aplikasi mobile, repository publik, atau log.",[19,461,463],{"id":462},"ganti-rotate-secret","Ganti (rotate) secret",[10,465,466,467,469],{},"Reset Secret Key di ",[142,468,161],{}," (butuh OTP ke email owner). Yang berubah:",[133,471,472,477],{},[136,473,474,475,155],{},"Webhook berikutnya, termasuk retry, langsung memakai secret baru di header ",[14,476,16],{},[136,478,479],{},"Panggilan Public API dengan secret lama ditolak.",[10,481,482],{},"Urutan aman:",[484,485,486,493,496],"ol",{},[136,487,488,489,492],{},"Siapkan server agar sementara menerima secret lama ",[142,490,491],{},"atau"," baru.",[136,494,495],{},"Reset secret di dashboard, lalu simpan secret baru di server.",[136,497,498],{},"Hapus secret lama dari server.",[19,500,502],{"id":501},"aturan-sukses","Aturan sukses",[10,504,505,506,509,510,513],{},"CSKU menganggap pengiriman ",[142,507,508],{},"sukses"," hanya jika body respons Anda berupa JSON dengan field ",[14,511,512],{},"success"," bernilai benar:",[24,515,519],{"className":516,"code":517,"language":518,"meta":29,"style":29},"language-json shiki shiki-themes github-light github-dark","{ \"success\": true }\n","json",[14,520,521],{"__ignoreMap":29},[33,522,523,526,529,532,535],{"class":35,"line":36},[33,524,525],{"class":43},"{ ",[33,527,528],{"class":53},"\"success\"",[33,530,531],{"class":43},": ",[33,533,534],{"class":53},"true",[33,536,537],{"class":43}," }\n",[10,539,540,541,124,543,546,547,550,551,554,555,558],{},"Nilai ",[14,542,534],{},[14,544,545],{},"\"true\"",", atau ",[14,548,549],{},"1"," diterima. Status HTTP ",[142,552,553],{},"tidak"," dibaca, jadi ",[14,556,557],{},"200"," dengan body kosong tetap dianggap gagal.",[10,560,561,562,64],{},"Dianggap ",[142,563,564],{},"gagal",[133,566,567,570,573,579],{},[136,568,569],{},"Tidak ada respons dalam 30 detik (timeout).",[136,571,572],{},"Koneksi gagal (DNS, TLS, server mati).",[136,574,575,576,578],{},"Body bukan JSON, atau field ",[14,577,512],{}," tidak ada.",[136,580,581,583,584,155],{},[14,582,512],{}," bernilai ",[14,585,586],{},"false",[19,588,590],{"id":589},"jadwal-retry","Jadwal retry",[10,592,593],{},"Pengiriman yang gagal dicoba ulang otomatis. Total maksimal 4 percobaan:",[595,596,597,610],"table",{},[598,599,600],"thead",{},[601,602,603,607],"tr",{},[604,605,606],"th",{},"Percobaan",[604,608,609],{},"Waktu (perkiraan)",[611,612,613,621,629,637],"tbody",{},[601,614,615,618],{},[616,617,549],"td",{},[616,619,620],{},"Saat event terjadi",[601,622,623,626],{},[616,624,625],{},"2",[616,627,628],{},"Langsung setelah percobaan 1 gagal",[601,630,631,634],{},[616,632,633],{},"3",[616,635,636],{},"Sekitar 30 detik setelah percobaan 2",[601,638,639,642],{},[616,640,641],{},"4",[616,643,644],{},"Sekitar 30 detik setelah percobaan 3",[10,646,647,648,155],{},"Setelah percobaan ke-4 gagal, event itu dilepas dan tidak dikirim lagi. Jika server Anda mati lebih dari sekitar satu menit, ambil pesan yang terlewat lewat ",[164,649,651],{"href":650},"\u002Fapi\u002Fambil-pesan","Ambil pesan",[19,653,655],{"id":654},"idempotensi-dan-urutan","Idempotensi dan urutan",[133,657,658,674],{},[136,659,660,666,667,669,670,673],{},[142,661,662,663,155],{},"Deduplikasi dengan ",[14,664,665],{},"message.id"," Event yang sama bisa datang lebih dari sekali, misalnya jika server Anda sudah memproses tetapi respons terlambat. Simpan ",[14,668,665],{}," yang sudah diproses dan abaikan duplikat. Jangan pakai ",[14,671,672],{},"timestamp",", karena nilainya berubah di setiap percobaan.",[136,675,676,679,680,155],{},[142,677,678],{},"Urutan tidak dijamin."," Setiap event dikirim sendiri-sendiri. Retry bisa membuat event lama datang setelah event baru. Jika urutan penting, urutkan di sisi Anda, atau ambil riwayat lewat ",[164,681,651],{"href":650},[19,683,685],{"id":684},"respons-cepat-proses-belakangan","Respons cepat, proses belakangan",[10,687,688],{},"Batas waktu 30 detik termasuk semua yang Anda kerjakan sebelum membalas. Pola yang aman:",[484,690,691,696,699,705],{},[136,692,693,694,155],{},"Verifikasi ",[14,695,16],{},[136,697,698],{},"Simpan payload ke antrean atau database.",[136,700,701,702,155],{},"Balas ",[14,703,704],{},"{\"success\": true}",[136,706,707],{},"Proses (panggil sistem lain, kirim notifikasi) di background job.",[19,709,711],{"id":710},"checklist-jika-webhook-tidak-masuk","Checklist jika webhook tidak masuk",[133,713,714,725,740,743,749,755,758,764,767],{},[136,715,716,717,720,721,155],{},"Paket Anda masih punya fitur ",[142,718,719],{},"Webhook"," dan masih aktif. Lihat ",[164,722,724],{"href":723},"\u002Fpanduan\u002Fberlangganan","Berlangganan & kredit",[136,726,727,728,731,732,735,736,739],{},"URL di ",[142,729,730],{},"Pengaturan → Webhook"," benar, memakai ",[14,733,734],{},"https",", dan bisa diakses dari internet (bukan ",[14,737,738],{},"localhost"," atau IP privat).",[136,741,742],{},"Sertifikat TLS server valid dan tidak kedaluwarsa.",[136,744,745,746,748],{},"Firewall atau WAF (misalnya proteksi bot) tidak memblokir request ",[14,747,40],{}," dari server CSKU.",[136,750,751,752,754],{},"Server membaca header ",[14,753,16],{}," dengan benar. Di Apache + PHP, header ini kadang tidak diteruskan.",[136,756,757],{},"Secret di server sama dengan Secret Key terbaru. Jika Anda baru reset, perbarui juga di server.",[136,759,760,761,763],{},"Respons Anda berupa JSON ",[14,762,704],{},", bukan teks atau body kosong.",[136,765,766],{},"Respons dikirim dalam waktu kurang dari 30 detik.",[136,768,769,770,162,773,155],{},"Event yang Anda tunggu memang dikirim. Pesan dari Public API dan Proxy WABA tidak memicu ",[14,771,772],{},"agent.message_out",[164,774,776],{"href":775},"\u002Fwebhook\u002Fevent","Event & payload",[778,779,780],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":29,"searchDepth":71,"depth":71,"links":782},[783,784,785,786,787,788,789,790],{"id":21,"depth":57,"text":22},{"id":170,"depth":57,"text":171},{"id":462,"depth":57,"text":463},{"id":501,"depth":57,"text":502},{"id":589,"depth":57,"text":590},{"id":654,"depth":57,"text":655},{"id":684,"depth":57,"text":685},{"id":710,"depth":57,"text":711},"Cara memverifikasi request webhook, aturan sukses, jadwal retry, dan checklist saat webhook tidak masuk.",null,"md",{},"\u002Fwebhook\u002Fkeamanan-retry",{"title":5,"description":791},"webhook\u002Fkeamanan-retry","jJXT34ELtlAKTYhxVPgjabeitVVybrTmHUEq1SZV_FQ",1791957694417]