[{"data":1,"prerenderedAt":2897},["ShallowReactive",2],{"navigation":3,"\u002Fapi\u002Fwebhooks\u002Fverifying-signatures":547,"\u002Fapi\u002Fwebhooks\u002Fverifying-signatures-surround":2892},[4,28,62,92,117,146,253,292,325,349,385,421,459,488],{"title":5,"path":6,"stem":7,"children":8,"icon":27},"Getting Started","\u002Fgetting-started","1.getting-started\u002F1.index",[9,12,17,22],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Usage","\u002Fgetting-started\u002Fusage","1.getting-started\u002F3.usage","i-lucide-sliders",{"title":18,"path":19,"stem":20,"icon":21},"Signing in on the mobile app","\u002Fgetting-started\u002Fmobile-app-sign-in","1.getting-started\u002F4.mobile-app-sign-in","i-lucide-smartphone",{"title":23,"path":24,"stem":25,"icon":26},"Running without email","\u002Fgetting-started\u002Frunning-without-email","1.getting-started\u002F5.running-without-email","i-lucide-mail-x",false,{"title":29,"icon":27,"path":30,"stem":31,"children":32,"page":27},"Security & Privacy","\u002Fsecurity","10.security",[33,38,43,47,52,57],{"title":34,"path":35,"stem":36,"icon":37},"Security Overview","\u002Fsecurity\u002Foverview","10.security\u002F1.overview","i-lucide-shield-check",{"title":39,"path":40,"stem":41,"icon":42},"Authentication","\u002Fsecurity\u002Fauthentication","10.security\u002F2.authentication","i-lucide-log-in",{"title":44,"path":45,"stem":46,"icon":21},"Two-Factor & Devices","\u002Fsecurity\u002Ftwo-factor-and-devices","10.security\u002F3.two-factor-and-devices",{"title":48,"path":49,"stem":50,"icon":51},"Audit & Monitoring","\u002Fsecurity\u002Faudit-and-monitoring","10.security\u002F4.audit-and-monitoring","i-lucide-scroll-text",{"title":53,"path":54,"stem":55,"icon":56},"Account Deletion & Privacy","\u002Fsecurity\u002Faccount-deletion-and-privacy","10.security\u002F5.account-deletion-and-privacy","i-lucide-user-x",{"title":58,"path":59,"stem":60,"icon":61},"Roles & Permissions","\u002Fsecurity\u002Froles-and-permissions","10.security\u002F6.roles-and-permissions","i-lucide-key-round",{"title":63,"icon":27,"path":64,"stem":65,"children":66,"page":27},"Billing & Plans","\u002Fbilling","11.billing",[67,72,77,82,87],{"title":68,"path":69,"stem":70,"icon":71},"Plans","\u002Fbilling\u002Fplans","11.billing\u002F1.plans","i-lucide-credit-card",{"title":73,"path":74,"stem":75,"icon":76},"Limits & Usage","\u002Fbilling\u002Flimits-and-usage","11.billing\u002F2.limits-and-usage","i-lucide-gauge",{"title":78,"path":79,"stem":80,"icon":81},"Managing Your Subscription","\u002Fbilling\u002Fmanaging-your-subscription","11.billing\u002F3.managing-your-subscription","i-lucide-receipt",{"title":83,"path":84,"stem":85,"icon":86},"Add-ons","\u002Fbilling\u002Fadd-ons","11.billing\u002F4.add-ons","i-lucide-puzzle",{"title":88,"path":89,"stem":90,"icon":91},"Integrations","\u002Fbilling\u002Fintegrations","11.billing\u002F5.integrations","i-lucide-plug",{"title":93,"icon":27,"path":94,"stem":95,"children":96,"page":27},"Settings","\u002Fsettings","12.settings",[97,101,105,109,113],{"title":98,"path":99,"stem":100},"General","\u002Fsettings\u002Fgeneral","12.settings\u002F1.general",{"title":102,"path":103,"stem":104},"Leave Types","\u002Fsettings\u002Fleavetype","12.settings\u002F2.leaveType",{"title":106,"path":107,"stem":108},"Carry Forward","\u002Fsettings\u002Fcarryforward","12.settings\u002F3.carryForward",{"title":110,"path":111,"stem":112},"Department Management","\u002Fsettings\u002Fdepartments","12.settings\u002F4.departments",{"title":114,"path":115,"stem":116},"Public Holidays","\u002Fsettings\u002Fpublicholiday","12.settings\u002F5.publicholiday",{"title":118,"path":119,"stem":120,"children":121,"page":27},"Mcp","\u002Fmcp","13.mcp",[122,127,131,136,141],{"title":123,"path":124,"stem":125,"icon":126},"Overview","\u002Fmcp\u002Foverview","13.mcp\u002F1.overview","i-lucide-bot",{"title":128,"path":129,"stem":130,"icon":91},"Connecting","\u002Fmcp\u002Fconnecting","13.mcp\u002F2.connecting",{"title":132,"path":133,"stem":134,"icon":135},"Tools Reference","\u002Fmcp\u002Ftools","13.mcp\u002F3.tools","i-lucide-wrench",{"title":137,"path":138,"stem":139,"icon":140},"Security","\u002Fmcp\u002Fsecurity","13.mcp\u002F4.security","i-lucide-shield",{"title":142,"path":143,"stem":144,"icon":145},"Integrating Your Own MCP Client","\u002Fmcp\u002Fcustom-client","13.mcp\u002F5.custom-client","i-lucide-code",{"title":147,"icon":27,"path":148,"stem":149,"children":150,"page":27},"Public API","\u002Fapi","14.api",[151,156,160,164,169,174,179,183,208,238,243,248],{"title":152,"path":153,"stem":154,"icon":155},"API Overview","\u002Fapi\u002Foverview","14.api\u002F1.overview","i-lucide-code-xml",{"title":5,"path":157,"stem":158,"icon":159},"\u002Fapi\u002Fgetting-started","14.api\u002F2.getting-started","i-lucide-rocket",{"title":161,"path":162,"stem":163,"icon":61},"Authentication & Tokens","\u002Fapi\u002Fauthentication","14.api\u002F3.authentication",{"title":165,"path":166,"stem":167,"icon":168},"API Roles & Scopes","\u002Fapi\u002Froles-and-scopes","14.api\u002F4.roles-and-scopes","i-lucide-layers",{"title":170,"path":171,"stem":172,"icon":173},"Requests & Responses","\u002Fapi\u002Frequests-and-responses","14.api\u002F5.requests-and-responses","i-lucide-arrow-left-right",{"title":175,"path":176,"stem":177,"icon":178},"Errors","\u002Fapi\u002Ferrors","14.api\u002F6.errors","i-lucide-circle-alert",{"title":180,"path":181,"stem":182,"icon":76},"Rate Limits","\u002Fapi\u002Frate-limits","14.api\u002F7.rate-limits",{"title":184,"icon":185,"path":186,"stem":187,"children":188,"page":27},"Guides","i-lucide-book-marked","\u002Fapi\u002Fguides","14.api\u002F8.guides",[189,194,199,204],{"title":190,"path":191,"stem":192,"icon":193},"People","\u002Fapi\u002Fguides\u002Fpeople","14.api\u002F8.guides\u002F1.people","i-lucide-users",{"title":195,"path":196,"stem":197,"icon":198},"Leave","\u002Fapi\u002Fguides\u002Fleave","14.api\u002F8.guides\u002F2.leave","i-lucide-calendar-days",{"title":200,"path":201,"stem":202,"icon":203},"Time","\u002Fapi\u002Fguides\u002Ftime","14.api\u002F8.guides\u002F3.time","i-lucide-clock",{"title":205,"path":206,"stem":207,"icon":81},"Expenses & Documents","\u002Fapi\u002Fguides\u002Fexpenses-and-documents","14.api\u002F8.guides\u002F4.expenses-and-documents",{"title":209,"icon":210,"path":211,"stem":212,"children":213,"page":27},"Webhooks","i-lucide-webhook","\u002Fapi\u002Fwebhooks","14.api\u002F9.webhooks",[214,218,223,228,233],{"title":215,"path":216,"stem":217,"icon":210},"Webhooks Overview","\u002Fapi\u002Fwebhooks\u002Foverview","14.api\u002F9.webhooks\u002F1.overview",{"title":219,"path":220,"stem":221,"icon":222},"Events Reference","\u002Fapi\u002Fwebhooks\u002Fevents","14.api\u002F9.webhooks\u002F2.events","i-lucide-list",{"title":224,"path":225,"stem":226,"icon":227},"Verifying Signatures","\u002Fapi\u002Fwebhooks\u002Fverifying-signatures","14.api\u002F9.webhooks\u002F3.verifying-signatures","i-lucide-badge-check",{"title":229,"path":230,"stem":231,"icon":232},"Retries & Failures","\u002Fapi\u002Fwebhooks\u002Fretries-and-failures","14.api\u002F9.webhooks\u002F4.retries-and-failures","i-lucide-refresh-cw",{"title":234,"path":235,"stem":236,"icon":237},"Best Practices","\u002Fapi\u002Fwebhooks\u002Fbest-practices","14.api\u002F9.webhooks\u002F5.best-practices","i-lucide-list-checks",{"title":239,"path":240,"stem":241,"icon":242},"Postman & OpenAPI","\u002Fapi\u002Fpostman-and-openapi","14.api\u002F91.postman-and-openapi","i-lucide-file-json",{"title":244,"path":245,"stem":246,"icon":247},"Versioning & Changes","\u002Fapi\u002Fversioning","14.api\u002F92.versioning","i-lucide-git-branch",{"title":249,"path":250,"stem":251,"icon":252},"API Reference","\u002Fapi\u002Freference","14.api\u002F93.reference","i-lucide-book-open",{"title":254,"icon":27,"path":255,"stem":256,"children":257,"page":27},"Data Import & Migration","\u002Fdata-import","2.data-import",[258,262,267,272,277,282,287],{"title":123,"path":259,"stem":260,"icon":261},"\u002Fdata-import\u002Foverview","2.data-import\u002F1.overview","i-lucide-database",{"title":263,"path":264,"stem":265,"icon":266},"Importing a File","\u002Fdata-import\u002Fimporting-a-file","2.data-import\u002F2.importing-a-file","i-lucide-file-up",{"title":268,"path":269,"stem":270,"icon":271},"Supported Platforms","\u002Fdata-import\u002Fsupported-platforms","2.data-import\u002F3.supported-platforms","i-lucide-layout-grid",{"title":273,"path":274,"stem":275,"icon":276},"Direct Connections","\u002Fdata-import\u002Fdirect-connections","2.data-import\u002F4.direct-connections","i-lucide-plug-zap",{"title":278,"path":279,"stem":280,"icon":281},"Custom Templates","\u002Fdata-import\u002Fcustom-templates","2.data-import\u002F5.custom-templates","i-lucide-layout-template",{"title":283,"path":284,"stem":285,"icon":286},"Rollback & Import History","\u002Fdata-import\u002Frollback-and-history","2.data-import\u002F6.rollback-and-history","i-lucide-undo-2",{"title":288,"path":289,"stem":290,"icon":291},"Troubleshooting","\u002Fdata-import\u002Ftroubleshooting","2.data-import\u002F7.troubleshooting","i-lucide-life-buoy",{"title":293,"icon":27,"path":294,"stem":295,"children":296,"page":27},"Leave Management","\u002Fleave-management","3.leave-management",[297,302,307,312,316,321],{"title":298,"path":299,"stem":300,"icon":301},"Leave Management Overview","\u002Fleave-management\u002Foverview","3.leave-management\u002F1.overview","i-lucide-palmtree",{"title":303,"path":304,"stem":305,"icon":306},"Requesting Leave","\u002Fleave-management\u002Frequesting-leave","3.leave-management\u002F2.requesting-leave","i-lucide-calendar-plus",{"title":308,"path":309,"stem":310,"icon":311},"Leave Balances","\u002Fleave-management\u002Fleave-balances","3.leave-management\u002F3.leave-balances","i-lucide-wallet",{"title":313,"path":314,"stem":315,"icon":193},"Group Booking","\u002Fleave-management\u002Fgroup-booking","3.leave-management\u002F4.group-booking",{"title":317,"path":318,"stem":319,"icon":320},"Editing and Cancelling Leave","\u002Fleave-management\u002Fediting-and-cancelling","3.leave-management\u002F5.editing-and-cancelling","i-lucide-pencil",{"title":322,"path":323,"stem":324,"icon":291},"Leave Troubleshooting","\u002Fleave-management\u002Ftroubleshooting","3.leave-management\u002F6.troubleshooting",{"title":326,"icon":27,"path":327,"stem":328,"children":329,"page":27},"Approvals","\u002Fapprovals","4.approvals",[330,335,340,344],{"title":331,"path":332,"stem":333,"icon":334},"Approvals Overview","\u002Fapprovals\u002Foverview","4.approvals\u002F1.overview","i-lucide-check-check",{"title":336,"path":337,"stem":338,"icon":339},"Leave Approvals","\u002Fapprovals\u002Fleave-approvals","4.approvals\u002F2.leave-approvals","i-lucide-calendar-check",{"title":341,"path":342,"stem":343,"icon":81},"Expense Approvals","\u002Fapprovals\u002Fexpense-approvals","4.approvals\u002F3.expense-approvals",{"title":345,"path":346,"stem":347,"icon":348},"Time Approvals","\u002Fapprovals\u002Ftime-approvals","4.approvals\u002F4.time-approvals","i-lucide-clock-9",{"title":350,"icon":27,"path":351,"stem":352,"children":353,"page":27},"Time tracking & monitoring","\u002Ftime-tracking","5.Time Tracking",[354,359,364,369,373,377,381],{"title":355,"path":356,"stem":357,"icon":358},"Clocking In & Location","\u002Ftime-tracking\u002Fclock-in-and-location","5.Time Tracking\u002F5.clock-in-and-location","i-lucide-map-pin",{"title":360,"path":361,"stem":362,"icon":363},"Overtime & Pay-Period Lockdown","\u002Ftime-tracking\u002Fovertime-and-lockdown","5.Time Tracking\u002F6.overtime-and-lockdown","i-lucide-lock",{"title":365,"path":366,"stem":367,"icon":368},"Geofencing","\u002Ftime-tracking\u002Fgeofencing","5.Time Tracking\u002F7.geofencing","i-lucide-map",{"title":370,"path":371,"stem":372},"Time & Projects","\u002Ftime-tracking\u002Ftimeandprojects","5.Time Tracking\u002FTime&Projects",{"title":374,"path":375,"stem":376},"Time Tracking","\u002Ftime-tracking\u002Ftime-tracking","5.Time Tracking\u002FTime-tracking",{"title":378,"path":379,"stem":380},"Timeboard","\u002Ftime-tracking\u002Ftimeboard","5.Time Tracking\u002FTimeBoard",{"title":382,"path":383,"stem":384},"Timesheets","\u002Ftime-tracking\u002Ftimesheets","5.Time Tracking\u002FTimesheets",{"title":386,"icon":27,"path":387,"stem":388,"children":389,"page":27},"Expenses","\u002Fexpenses","6.expenses",[390,394,398,402,406,411,416],{"title":391,"path":392,"stem":393},"Expense Claims","\u002Fexpenses\u002Foverview","6.expenses\u002F1.overview",{"title":395,"path":396,"stem":397},"Add & Manage Purchases","\u002Fexpenses\u002Fpurchases","6.expenses\u002F2.purchases",{"title":399,"path":400,"stem":401},"Travel & Mileage Entry","\u002Fexpenses\u002Ftravelentries","6.expenses\u002F3.travelentries",{"title":403,"path":404,"stem":405,"icon":71},"Company Card Expenses","\u002Fexpenses\u002Fcompany-cards","6.expenses\u002F4.company-cards",{"title":407,"path":408,"stem":409,"icon":410},"Per Diems","\u002Fexpenses\u002Fper-diems","6.expenses\u002F5.per-diems","i-lucide-utensils",{"title":412,"path":413,"stem":414,"icon":415},"Receipt Scanning","\u002Fexpenses\u002Freceipt-scanning","6.expenses\u002F6.receipt-scanning","i-lucide-scan-line",{"title":417,"path":418,"stem":419,"icon":420},"Statuses & Submission","\u002Fexpenses\u002Fstatuses-and-submission","6.expenses\u002F7.statuses-and-submission","i-lucide-git-pull-request-arrow",{"title":422,"icon":27,"path":423,"stem":424,"children":425,"page":27},"Documents & E-Signatures","\u002Fdocuments","7.documents",[426,430,435,440,445,449,454],{"title":123,"path":427,"stem":428,"icon":429},"\u002Fdocuments\u002Foverview","7.documents\u002F1.overview","i-lucide-folder-open",{"title":431,"path":432,"stem":433,"icon":434},"Uploading Documents","\u002Fdocuments\u002Fuploading","7.documents\u002F2.uploading","i-lucide-upload",{"title":436,"path":437,"stem":438,"icon":439},"E-Signatures","\u002Fdocuments\u002Fe-signatures","7.documents\u002F3.e-signatures","i-lucide-pen-tool",{"title":441,"path":442,"stem":443,"icon":444},"Audit Trail & Certificate","\u002Fdocuments\u002Faudit-trail","7.documents\u002F4.audit-trail","i-lucide-file-check",{"title":446,"path":447,"stem":448,"icon":281},"Templates & Bulk Send","\u002Fdocuments\u002Ftemplates-and-bulk-send","7.documents\u002F5.templates-and-bulk-send",{"title":450,"path":451,"stem":452,"icon":453},"Onboarding","\u002Fdocuments\u002Fonboarding","7.documents\u002F6.onboarding","i-lucide-user-plus",{"title":455,"path":456,"stem":457,"icon":458},"Offboarding","\u002Fdocuments\u002Foffboarding","7.documents\u002F7.offboarding","i-lucide-user-minus",{"title":460,"icon":27,"path":461,"stem":462,"children":463,"page":27},"People & HR","\u002Fpeople","8.people",[464,468,473,478,483],{"title":465,"path":466,"stem":467,"icon":193},"Directory & Profiles","\u002Fpeople\u002Fdirectory-and-profiles","8.people\u002F1.directory-and-profiles",{"title":469,"path":470,"stem":471,"icon":472},"Managing Users","\u002Fpeople\u002Fmanaging-users","8.people\u002F2.managing-users","i-lucide-user-cog",{"title":474,"path":475,"stem":476,"icon":477},"HR Records","\u002Fpeople\u002Fhr-records","8.people\u002F3.hr-records","i-lucide-folder-archive",{"title":479,"path":480,"stem":481,"icon":482},"Performance","\u002Fpeople\u002Fperformance","8.people\u002F4.performance","i-lucide-target",{"title":484,"path":485,"stem":486,"icon":487},"Training","\u002Fpeople\u002Ftraining","8.people\u002F5.training","i-lucide-graduation-cap",{"title":489,"icon":27,"path":490,"stem":491,"children":492,"page":27},"Features","\u002Ffeatures","9.features",[493,497,501,505,510,515,519,523,527,531,535,539,543],{"title":494,"path":495,"stem":496,"icon":11},"Home","\u002Ffeatures\u002Fhome","9.features\u002F1.home",{"title":498,"path":499,"stem":500},"Email Configuration (SMTP)","\u002Ffeatures\u002Femail-configuration","9.features\u002F10.email-configuration",{"title":502,"path":503,"stem":504},"SMTP troubleshoot","\u002Ffeatures\u002Fsmtp-troubleshoot","9.features\u002F11.smtp-troubleshoot",{"title":506,"path":507,"stem":508,"icon":509},"Notifications","\u002Ffeatures\u002Fnotifications","9.features\u002F12.notifications","i-lucide-bell",{"title":511,"path":512,"stem":513,"icon":514},"Reports","\u002Ffeatures\u002Freports","9.features\u002F13.reports","i-lucide-bar-chart-3",{"title":516,"path":517,"stem":518},"Dashboard","\u002Ffeatures\u002Fdashboard","9.features\u002F2.dashboard",{"title":520,"path":521,"stem":522},"Calendar & Leave Overview","\u002Ffeatures\u002Fcalendar","9.features\u002F3.calendar",{"title":524,"path":525,"stem":526},"Calendar Integration","\u002Ffeatures\u002Fcalendar-integration","9.features\u002F4.calendar-integration",{"title":528,"path":529,"stem":530},"Schedules","\u002Ffeatures\u002Fschedules","9.features\u002F5.schedules",{"title":532,"path":533,"stem":534},"Project Management","\u002Ffeatures\u002Fprojects","9.features\u002F6.projects",{"title":536,"path":537,"stem":538},"Expense Settings","\u002Ffeatures\u002Fexpense-settings","9.features\u002F7.expense-settings",{"title":540,"path":541,"stem":542},"Auth0 SSO Integration","\u002Ffeatures\u002Fauth0-integration","9.features\u002F8.auth0-integration",{"title":544,"path":545,"stem":546},"Password & Authentication Policy","\u002Ffeatures\u002Fpolicies","9.features\u002F9.policies",{"id":548,"title":224,"body":549,"description":2885,"extension":2886,"links":2887,"meta":2888,"navigation":2889,"path":225,"seo":2890,"stem":226,"__hash__":2891},"docs\u002F14.api\u002F9.webhooks\u002F3.verifying-signatures.md",{"type":550,"value":551,"toc":2875},"minimark",[552,556,568,573,631,642,646,680,685,688,742,926,932,940,944,992,996,999,2769,2773,2776,2801,2808,2811,2814,2871],[553,554,555],"p",{},"Anyone who learns your endpoint's URL can send it requests. Verify the signature on every delivery before you act on it, and reject anything that fails.",[553,557,558,559,567],{},"Signatures follow the open ",[560,561,566],"a",{"href":562,"rel":563,"target":565},"https:\u002F\u002Fwww.standardwebhooks.com",[564],"nofollow","_blank","Standard Webhooks"," scheme, so you can also use one of the libraries that implement it for your language.",[569,570,572],"h2",{"id":571},"the-headers","The headers",[574,575,576,589],"table",{},[577,578,579],"thead",{},[580,581,582,586],"tr",{},[583,584,585],"th",{},"Header",[583,587,588],{},"Value",[590,591,592,608,618],"tbody",{},[580,593,594,601],{},[595,596,597],"td",{},[598,599,600],"code",{},"webhook-id",[595,602,603,604,607],{},"The event id, the same as ",[598,605,606],{},"id"," in the body. It does not change between retries.",[580,609,610,615],{},[595,611,612],{},[598,613,614],{},"webhook-timestamp",[595,616,617],{},"When this attempt was signed, in Unix seconds. Every attempt, including a retry or a redelivery, is signed afresh with a new timestamp.",[580,619,620,625],{},[595,621,622],{},[598,623,624],{},"webhook-signature",[595,626,627,628],{},"One or more signatures, separated by spaces, each in the form ",[598,629,630],{},"v1,\u003Csignature>",[553,632,633,634,637,638,641],{},"Every delivery also carries ",[598,635,636],{},"Content-Type: application\u002Fjson"," and ",[598,639,640],{},"User-Agent: Webhooks\u002F1.0",".",[569,643,645],{"id":644},"how-the-signature-is-computed","How the signature is computed",[647,648,649,657,668,671],"ol",{},[650,651,652,653,656],"li",{},"Take the signing secret, remove the ",[598,654,655],{},"whsec_"," prefix and base64-decode the rest. The result is the key.",[650,658,659,660,664,665,641],{},"Build the signed content by joining the id, the timestamp and the ",[661,662,663],"strong",{},"raw"," request body with full stops: ",[598,666,667],{},"{webhook-id}.{webhook-timestamp}.{raw body}",[650,669,670],{},"Compute HMAC-SHA256 of the signed content with the key, and base64-encode the result.",[650,672,673,674,677,678,641],{},"The delivery is genuine if that value equals any ",[598,675,676],{},"v1"," signature in ",[598,679,624],{},[681,682,684],"h3",{"id":683},"test-vector","Test vector",[553,686,687],{},"Use these values to check step 1 to 3 of your implementation. The timestamp is fixed, so switch the timestamp check off for this test only.",[574,689,690,699],{},[577,691,692],{},[580,693,694,697],{},[583,695,696],{},"Input",[583,698,588],{},[590,700,701,712,723,734],{},[580,702,703,706],{},[595,704,705],{},"Secret",[595,707,708,711],{},[598,709,710],{},"whsec_YSBmaXhlZCBleGFtcGxlIGtleSwgbmV2ZXIgdXNlIGl0"," (an example, never use it)",[580,713,714,718],{},[595,715,716],{},[598,717,600],{},[595,719,720],{},[598,721,722],{},"evt_01JABCDEFGHJKMNPQRSTVWXYZ",[580,724,725,729],{},[595,726,727],{},[598,728,614],{},[595,730,731],{},[598,732,733],{},"1791824652",[580,735,736,739],{},[595,737,738],{},"Raw body",[595,740,741],{},"The single line below, with no trailing newline",[743,744,749],"pre",{"className":745,"code":746,"language":747,"meta":748,"style":748},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\"id\":\"evt_01JABCDEFGHJKMNPQRSTVWXYZ\",\"type\":\"webhook.test\",\"api_version\":\"v1\",\"created_at\":\"2026-10-12T17:04:11.512Z\",\"organization_id\":\"cmorg0000000000000000000\",\"data\":{\"object\":{\"id\":\"cmwhk0000000000000000000\",\"object\":\"webhook_endpoint\",\"message\":\"A test event, sent on request to check that this endpoint receives and verifies events.\"}}}\n","json","",[598,750,751],{"__ignoreMap":748},[752,753,756,760,763,766,768,771,773,776,778,781,783,786,788,790,792,795,797,799,801,804,806,808,810,812,814,816,818,821,823,825,827,830,832,834,836,839,841,843,845,848,850,852,854,857,859,862,864,868,870,872,874,877,879,881,883,886,888,890,892,894,896,898,900,903,905,907,909,912,914,916,918,921,923],"span",{"class":754,"line":755},"line",1,[752,757,759],{"class":758},"sMK4o","{",[752,761,762],{"class":758},"\"",[752,764,606],{"class":765},"spNyl",[752,767,762],{"class":758},[752,769,770],{"class":758},":",[752,772,762],{"class":758},[752,774,722],{"class":775},"sfazB",[752,777,762],{"class":758},[752,779,780],{"class":758},",",[752,782,762],{"class":758},[752,784,785],{"class":765},"type",[752,787,762],{"class":758},[752,789,770],{"class":758},[752,791,762],{"class":758},[752,793,794],{"class":775},"webhook.test",[752,796,762],{"class":758},[752,798,780],{"class":758},[752,800,762],{"class":758},[752,802,803],{"class":765},"api_version",[752,805,762],{"class":758},[752,807,770],{"class":758},[752,809,762],{"class":758},[752,811,676],{"class":775},[752,813,762],{"class":758},[752,815,780],{"class":758},[752,817,762],{"class":758},[752,819,820],{"class":765},"created_at",[752,822,762],{"class":758},[752,824,770],{"class":758},[752,826,762],{"class":758},[752,828,829],{"class":775},"2026-10-12T17:04:11.512Z",[752,831,762],{"class":758},[752,833,780],{"class":758},[752,835,762],{"class":758},[752,837,838],{"class":765},"organization_id",[752,840,762],{"class":758},[752,842,770],{"class":758},[752,844,762],{"class":758},[752,846,847],{"class":775},"cmorg0000000000000000000",[752,849,762],{"class":758},[752,851,780],{"class":758},[752,853,762],{"class":758},[752,855,856],{"class":765},"data",[752,858,762],{"class":758},[752,860,861],{"class":758},":{",[752,863,762],{"class":758},[752,865,867],{"class":866},"sBMFI","object",[752,869,762],{"class":758},[752,871,861],{"class":758},[752,873,762],{"class":758},[752,875,606],{"class":876},"sbssI",[752,878,762],{"class":758},[752,880,770],{"class":758},[752,882,762],{"class":758},[752,884,885],{"class":775},"cmwhk0000000000000000000",[752,887,762],{"class":758},[752,889,780],{"class":758},[752,891,762],{"class":758},[752,893,867],{"class":876},[752,895,762],{"class":758},[752,897,770],{"class":758},[752,899,762],{"class":758},[752,901,902],{"class":775},"webhook_endpoint",[752,904,762],{"class":758},[752,906,780],{"class":758},[752,908,762],{"class":758},[752,910,911],{"class":876},"message",[752,913,762],{"class":758},[752,915,770],{"class":758},[752,917,762],{"class":758},[752,919,920],{"class":775},"A test event, sent on request to check that this endpoint receives and verifies events.",[752,922,762],{"class":758},[752,924,925],{"class":758},"}}}\n",[553,927,928,929,931],{},"The expected ",[598,930,624],{}," value is:",[743,933,938],{"className":934,"code":936,"language":937},[935],"language-text","v1,7\u002FSrARBfCegLSNnPiRNB2zj\u002FIi8MbpAkGAy7mhDmVN8=\n","text",[598,939,936],{"__ignoreMap":748},[569,941,943],{"id":942},"rules-for-verifying","Rules for verifying",[945,946,947,953,962,975,981],"ul",{},[650,948,949,952],{},[661,950,951],{},"Use the raw body exactly as received."," Parsing the JSON and serializing it again changes whitespace and key order, and the signature will not match. Read the body as bytes before any JSON middleware touches it.",[650,954,955,958,959,641],{},[661,956,957],{},"Compare in constant time",", using your language's constant-time comparison, never ",[598,960,961],{},"==",[650,963,964,967,968,970,971,974],{},[661,965,966],{},"Reject old or future timestamps."," Refuse a delivery whose ",[598,969,614],{}," is more than ",[661,972,973],{},"5 minutes"," away from your server's clock, in either direction. This stops a captured request from being replayed later. Keep your server's clock synchronised.",[650,976,977,980],{},[661,978,979],{},"Accept any matching signature."," While a secret is being rotated, each delivery carries two signatures, one for the old secret and one for the new, separated by a space. Accept the delivery if either matches the secret you hold.",[650,982,983,984,987,988,991],{},"Respond with ",[598,985,986],{},"401"," or ",[598,989,990],{},"400"," when verification fails, and do not process the payload.",[569,993,995],{"id":994},"code-samples","Code samples",[553,997,998],{},"Each sample returns successfully only for a genuine, fresh delivery. Load the secret from your secret store, never from source code.",[1000,1001,1002,1800,1968,2126,2307,2581],"code-group",{},[743,1003,1008],{"className":1004,"code":1005,"filename":1006,"language":1007,"meta":748,"style":748},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import crypto from 'node:crypto'\n\nconst TOLERANCE_SECONDS = 5 * 60\n\n\u002F\u002F rawBody: the request body as a Buffer, before JSON parsing\n\u002F\u002F (with Express: app.post('\u002Fwebhooks', express.raw({ type: 'application\u002Fjson' }), ...))\nexport function verifyWebhook(rawBody, headers, secret) {\n  const id = headers['webhook-id']\n  const timestamp = headers['webhook-timestamp']\n  const signatures = headers['webhook-signature']\n  if (!id || !timestamp || !signatures) throw new Error('Missing webhook headers')\n\n  const ts = Number(timestamp)\n  const now = Math.floor(Date.now() \u002F 1000)\n  if (!Number.isInteger(ts) || Math.abs(now - ts) > TOLERANCE_SECONDS) {\n    throw new Error('Timestamp outside the tolerance')\n  }\n\n  const key = Buffer.from(secret.replace(\u002F^whsec_\u002F, ''), 'base64')\n  const expected = crypto\n    .createHmac('sha256', key)\n    .update(`${id}.${timestamp}.`)\n    .update(rawBody)\n    .digest()\n\n  for (const entry of signatures.split(' ')) {\n    const [version, signature] = entry.split(',')\n    if (version !== 'v1' || !signature) continue\n    const given = Buffer.from(signature, 'base64')\n    if (given.length === expected.length && crypto.timingSafeEqual(given, expected)) {\n      return JSON.parse(rawBody.toString('utf8'))\n    }\n  }\n  throw new Error('No matching signature')\n}\n","Node.js","js",[598,1009,1010,1032,1039,1060,1065,1072,1078,1114,1142,1164,1186,1240,1245,1264,1303,1359,1380,1386,1391,1447,1460,1484,1518,1531,1542,1547,1581,1619,1650,1680,1727,1762,1768,1773,1794],{"__ignoreMap":748},[752,1011,1012,1016,1020,1023,1026,1029],{"class":754,"line":755},[752,1013,1015],{"class":1014},"s7zQu","import",[752,1017,1019],{"class":1018},"sTEyZ"," crypto ",[752,1021,1022],{"class":1014},"from",[752,1024,1025],{"class":758}," '",[752,1027,1028],{"class":775},"node:crypto",[752,1030,1031],{"class":758},"'\n",[752,1033,1035],{"class":754,"line":1034},2,[752,1036,1038],{"emptyLinePlaceholder":1037},true,"\n",[752,1040,1042,1045,1048,1051,1054,1057],{"class":754,"line":1041},3,[752,1043,1044],{"class":765},"const",[752,1046,1047],{"class":1018}," TOLERANCE_SECONDS ",[752,1049,1050],{"class":758},"=",[752,1052,1053],{"class":876}," 5",[752,1055,1056],{"class":758}," *",[752,1058,1059],{"class":876}," 60\n",[752,1061,1063],{"class":754,"line":1062},4,[752,1064,1038],{"emptyLinePlaceholder":1037},[752,1066,1068],{"class":754,"line":1067},5,[752,1069,1071],{"class":1070},"sHwdD","\u002F\u002F rawBody: the request body as a Buffer, before JSON parsing\n",[752,1073,1075],{"class":754,"line":1074},6,[752,1076,1077],{"class":1070},"\u002F\u002F (with Express: app.post('\u002Fwebhooks', express.raw({ type: 'application\u002Fjson' }), ...))\n",[752,1079,1081,1084,1087,1091,1094,1098,1100,1103,1105,1108,1111],{"class":754,"line":1080},7,[752,1082,1083],{"class":1014},"export",[752,1085,1086],{"class":765}," function",[752,1088,1090],{"class":1089},"s2Zo4"," verifyWebhook",[752,1092,1093],{"class":758},"(",[752,1095,1097],{"class":1096},"sHdIc","rawBody",[752,1099,780],{"class":758},[752,1101,1102],{"class":1096}," headers",[752,1104,780],{"class":758},[752,1106,1107],{"class":1096}," secret",[752,1109,1110],{"class":758},")",[752,1112,1113],{"class":758}," {\n",[752,1115,1117,1120,1123,1126,1128,1132,1135,1137,1139],{"class":754,"line":1116},8,[752,1118,1119],{"class":765},"  const",[752,1121,1122],{"class":1018}," id",[752,1124,1125],{"class":758}," =",[752,1127,1102],{"class":1018},[752,1129,1131],{"class":1130},"swJcz","[",[752,1133,1134],{"class":758},"'",[752,1136,600],{"class":775},[752,1138,1134],{"class":758},[752,1140,1141],{"class":1130},"]\n",[752,1143,1145,1147,1150,1152,1154,1156,1158,1160,1162],{"class":754,"line":1144},9,[752,1146,1119],{"class":765},[752,1148,1149],{"class":1018}," timestamp",[752,1151,1125],{"class":758},[752,1153,1102],{"class":1018},[752,1155,1131],{"class":1130},[752,1157,1134],{"class":758},[752,1159,614],{"class":775},[752,1161,1134],{"class":758},[752,1163,1141],{"class":1130},[752,1165,1167,1169,1172,1174,1176,1178,1180,1182,1184],{"class":754,"line":1166},10,[752,1168,1119],{"class":765},[752,1170,1171],{"class":1018}," signatures",[752,1173,1125],{"class":758},[752,1175,1102],{"class":1018},[752,1177,1131],{"class":1130},[752,1179,1134],{"class":758},[752,1181,624],{"class":775},[752,1183,1134],{"class":758},[752,1185,1141],{"class":1130},[752,1187,1189,1192,1195,1198,1200,1203,1206,1209,1211,1213,1216,1219,1222,1225,1228,1230,1232,1235,1237],{"class":754,"line":1188},11,[752,1190,1191],{"class":1014},"  if",[752,1193,1194],{"class":1130}," (",[752,1196,1197],{"class":758},"!",[752,1199,606],{"class":1018},[752,1201,1202],{"class":758}," ||",[752,1204,1205],{"class":758}," !",[752,1207,1208],{"class":1018},"timestamp",[752,1210,1202],{"class":758},[752,1212,1205],{"class":758},[752,1214,1215],{"class":1018},"signatures",[752,1217,1218],{"class":1130},") ",[752,1220,1221],{"class":1014},"throw",[752,1223,1224],{"class":758}," new",[752,1226,1227],{"class":1089}," Error",[752,1229,1093],{"class":1130},[752,1231,1134],{"class":758},[752,1233,1234],{"class":775},"Missing webhook headers",[752,1236,1134],{"class":758},[752,1238,1239],{"class":1130},")\n",[752,1241,1243],{"class":754,"line":1242},12,[752,1244,1038],{"emptyLinePlaceholder":1037},[752,1246,1248,1250,1253,1255,1258,1260,1262],{"class":754,"line":1247},13,[752,1249,1119],{"class":765},[752,1251,1252],{"class":1018}," ts",[752,1254,1125],{"class":758},[752,1256,1257],{"class":1089}," Number",[752,1259,1093],{"class":1130},[752,1261,1208],{"class":1018},[752,1263,1239],{"class":1130},[752,1265,1267,1269,1272,1274,1277,1279,1282,1284,1287,1289,1292,1295,1298,1301],{"class":754,"line":1266},14,[752,1268,1119],{"class":765},[752,1270,1271],{"class":1018}," now",[752,1273,1125],{"class":758},[752,1275,1276],{"class":1018}," Math",[752,1278,641],{"class":758},[752,1280,1281],{"class":1089},"floor",[752,1283,1093],{"class":1130},[752,1285,1286],{"class":1018},"Date",[752,1288,641],{"class":758},[752,1290,1291],{"class":1089},"now",[752,1293,1294],{"class":1130},"() ",[752,1296,1297],{"class":758},"\u002F",[752,1299,1300],{"class":876}," 1000",[752,1302,1239],{"class":1130},[752,1304,1306,1308,1310,1312,1315,1317,1320,1322,1325,1327,1330,1332,1334,1337,1339,1341,1344,1346,1348,1351,1354,1356],{"class":754,"line":1305},15,[752,1307,1191],{"class":1014},[752,1309,1194],{"class":1130},[752,1311,1197],{"class":758},[752,1313,1314],{"class":1018},"Number",[752,1316,641],{"class":758},[752,1318,1319],{"class":1089},"isInteger",[752,1321,1093],{"class":1130},[752,1323,1324],{"class":1018},"ts",[752,1326,1218],{"class":1130},[752,1328,1329],{"class":758},"||",[752,1331,1276],{"class":1018},[752,1333,641],{"class":758},[752,1335,1336],{"class":1089},"abs",[752,1338,1093],{"class":1130},[752,1340,1291],{"class":1018},[752,1342,1343],{"class":758}," -",[752,1345,1252],{"class":1018},[752,1347,1218],{"class":1130},[752,1349,1350],{"class":758},">",[752,1352,1353],{"class":1018}," TOLERANCE_SECONDS",[752,1355,1218],{"class":1130},[752,1357,1358],{"class":758},"{\n",[752,1360,1362,1365,1367,1369,1371,1373,1376,1378],{"class":754,"line":1361},16,[752,1363,1364],{"class":1014},"    throw",[752,1366,1224],{"class":758},[752,1368,1227],{"class":1089},[752,1370,1093],{"class":1130},[752,1372,1134],{"class":758},[752,1374,1375],{"class":775},"Timestamp outside the tolerance",[752,1377,1134],{"class":758},[752,1379,1239],{"class":1130},[752,1381,1383],{"class":754,"line":1382},17,[752,1384,1385],{"class":758},"  }\n",[752,1387,1389],{"class":754,"line":1388},18,[752,1390,1038],{"emptyLinePlaceholder":1037},[752,1392,1394,1396,1399,1401,1404,1406,1408,1410,1413,1415,1418,1420,1422,1425,1427,1429,1431,1434,1436,1438,1440,1443,1445],{"class":754,"line":1393},19,[752,1395,1119],{"class":765},[752,1397,1398],{"class":1018}," key",[752,1400,1125],{"class":758},[752,1402,1403],{"class":1018}," Buffer",[752,1405,641],{"class":758},[752,1407,1022],{"class":1089},[752,1409,1093],{"class":1130},[752,1411,1412],{"class":1018},"secret",[752,1414,641],{"class":758},[752,1416,1417],{"class":1089},"replace",[752,1419,1093],{"class":1130},[752,1421,1297],{"class":758},[752,1423,1424],{"class":1014},"^",[752,1426,655],{"class":775},[752,1428,1297],{"class":758},[752,1430,780],{"class":758},[752,1432,1433],{"class":758}," ''",[752,1435,1110],{"class":1130},[752,1437,780],{"class":758},[752,1439,1025],{"class":758},[752,1441,1442],{"class":775},"base64",[752,1444,1134],{"class":758},[752,1446,1239],{"class":1130},[752,1448,1450,1452,1455,1457],{"class":754,"line":1449},20,[752,1451,1119],{"class":765},[752,1453,1454],{"class":1018}," expected",[752,1456,1125],{"class":758},[752,1458,1459],{"class":1018}," crypto\n",[752,1461,1463,1466,1469,1471,1473,1476,1478,1480,1482],{"class":754,"line":1462},21,[752,1464,1465],{"class":758},"    .",[752,1467,1468],{"class":1089},"createHmac",[752,1470,1093],{"class":1130},[752,1472,1134],{"class":758},[752,1474,1475],{"class":775},"sha256",[752,1477,1134],{"class":758},[752,1479,780],{"class":758},[752,1481,1398],{"class":1018},[752,1483,1239],{"class":1130},[752,1485,1487,1489,1492,1494,1497,1499,1502,1504,1507,1509,1511,1513,1516],{"class":754,"line":1486},22,[752,1488,1465],{"class":758},[752,1490,1491],{"class":1089},"update",[752,1493,1093],{"class":1130},[752,1495,1496],{"class":758},"`${",[752,1498,606],{"class":1018},[752,1500,1501],{"class":758},"}",[752,1503,641],{"class":775},[752,1505,1506],{"class":758},"${",[752,1508,1208],{"class":1018},[752,1510,1501],{"class":758},[752,1512,641],{"class":775},[752,1514,1515],{"class":758},"`",[752,1517,1239],{"class":1130},[752,1519,1521,1523,1525,1527,1529],{"class":754,"line":1520},23,[752,1522,1465],{"class":758},[752,1524,1491],{"class":1089},[752,1526,1093],{"class":1130},[752,1528,1097],{"class":1018},[752,1530,1239],{"class":1130},[752,1532,1534,1536,1539],{"class":754,"line":1533},24,[752,1535,1465],{"class":758},[752,1537,1538],{"class":1089},"digest",[752,1540,1541],{"class":1130},"()\n",[752,1543,1545],{"class":754,"line":1544},25,[752,1546,1038],{"emptyLinePlaceholder":1037},[752,1548,1550,1553,1555,1557,1560,1563,1565,1567,1570,1572,1574,1576,1579],{"class":754,"line":1549},26,[752,1551,1552],{"class":1014},"  for",[752,1554,1194],{"class":1130},[752,1556,1044],{"class":765},[752,1558,1559],{"class":1018}," entry",[752,1561,1562],{"class":758}," of",[752,1564,1171],{"class":1018},[752,1566,641],{"class":758},[752,1568,1569],{"class":1089},"split",[752,1571,1093],{"class":1130},[752,1573,1134],{"class":758},[752,1575,1025],{"class":758},[752,1577,1578],{"class":1130},")) ",[752,1580,1358],{"class":758},[752,1582,1584,1587,1590,1593,1595,1598,1601,1603,1605,1607,1609,1611,1613,1615,1617],{"class":754,"line":1583},27,[752,1585,1586],{"class":765},"    const",[752,1588,1589],{"class":758}," [",[752,1591,1592],{"class":1018},"version",[752,1594,780],{"class":758},[752,1596,1597],{"class":1018}," signature",[752,1599,1600],{"class":758},"]",[752,1602,1125],{"class":758},[752,1604,1559],{"class":1018},[752,1606,641],{"class":758},[752,1608,1569],{"class":1089},[752,1610,1093],{"class":1130},[752,1612,1134],{"class":758},[752,1614,780],{"class":775},[752,1616,1134],{"class":758},[752,1618,1239],{"class":1130},[752,1620,1622,1625,1627,1629,1632,1634,1636,1638,1640,1642,1645,1647],{"class":754,"line":1621},28,[752,1623,1624],{"class":1014},"    if",[752,1626,1194],{"class":1130},[752,1628,1592],{"class":1018},[752,1630,1631],{"class":758}," !==",[752,1633,1025],{"class":758},[752,1635,676],{"class":775},[752,1637,1134],{"class":758},[752,1639,1202],{"class":758},[752,1641,1205],{"class":758},[752,1643,1644],{"class":1018},"signature",[752,1646,1218],{"class":1130},[752,1648,1649],{"class":1014},"continue\n",[752,1651,1653,1655,1658,1660,1662,1664,1666,1668,1670,1672,1674,1676,1678],{"class":754,"line":1652},29,[752,1654,1586],{"class":765},[752,1656,1657],{"class":1018}," given",[752,1659,1125],{"class":758},[752,1661,1403],{"class":1018},[752,1663,641],{"class":758},[752,1665,1022],{"class":1089},[752,1667,1093],{"class":1130},[752,1669,1644],{"class":1018},[752,1671,780],{"class":758},[752,1673,1025],{"class":758},[752,1675,1442],{"class":775},[752,1677,1134],{"class":758},[752,1679,1239],{"class":1130},[752,1681,1683,1685,1687,1690,1692,1695,1698,1700,1702,1704,1707,1710,1712,1715,1717,1719,1721,1723,1725],{"class":754,"line":1682},30,[752,1684,1624],{"class":1014},[752,1686,1194],{"class":1130},[752,1688,1689],{"class":1018},"given",[752,1691,641],{"class":758},[752,1693,1694],{"class":1018},"length",[752,1696,1697],{"class":758}," ===",[752,1699,1454],{"class":1018},[752,1701,641],{"class":758},[752,1703,1694],{"class":1018},[752,1705,1706],{"class":758}," &&",[752,1708,1709],{"class":1018}," crypto",[752,1711,641],{"class":758},[752,1713,1714],{"class":1089},"timingSafeEqual",[752,1716,1093],{"class":1130},[752,1718,1689],{"class":1018},[752,1720,780],{"class":758},[752,1722,1454],{"class":1018},[752,1724,1578],{"class":1130},[752,1726,1358],{"class":758},[752,1728,1730,1733,1736,1738,1741,1743,1745,1747,1750,1752,1754,1757,1759],{"class":754,"line":1729},31,[752,1731,1732],{"class":1014},"      return",[752,1734,1735],{"class":1018}," JSON",[752,1737,641],{"class":758},[752,1739,1740],{"class":1089},"parse",[752,1742,1093],{"class":1130},[752,1744,1097],{"class":1018},[752,1746,641],{"class":758},[752,1748,1749],{"class":1089},"toString",[752,1751,1093],{"class":1130},[752,1753,1134],{"class":758},[752,1755,1756],{"class":775},"utf8",[752,1758,1134],{"class":758},[752,1760,1761],{"class":1130},"))\n",[752,1763,1765],{"class":754,"line":1764},32,[752,1766,1767],{"class":758},"    }\n",[752,1769,1771],{"class":754,"line":1770},33,[752,1772,1385],{"class":758},[752,1774,1776,1779,1781,1783,1785,1787,1790,1792],{"class":754,"line":1775},34,[752,1777,1778],{"class":1014},"  throw",[752,1780,1224],{"class":758},[752,1782,1227],{"class":1089},[752,1784,1093],{"class":1130},[752,1786,1134],{"class":758},[752,1788,1789],{"class":775},"No matching signature",[752,1791,1134],{"class":758},[752,1793,1239],{"class":1130},[752,1795,1797],{"class":754,"line":1796},35,[752,1798,1799],{"class":758},"}\n",[743,1801,1806],{"className":1802,"code":1803,"filename":1804,"language":1805,"meta":748,"style":748},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Python 3.9 or later\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport time\n\nTOLERANCE_SECONDS = 5 * 60\n\ndef verify_webhook(raw_body: bytes, headers, secret: str):\n    \"\"\"raw_body: the request body as bytes, before JSON parsing.\"\"\"\n    msg_id = headers.get(\"webhook-id\")\n    timestamp = headers.get(\"webhook-timestamp\")\n    signatures = headers.get(\"webhook-signature\")\n    if not msg_id or not timestamp or not signatures:\n        raise ValueError(\"Missing webhook headers\")\n\n    try:\n        ts = int(timestamp)\n    except ValueError:\n        raise ValueError(\"Invalid timestamp\")\n    if abs(int(time.time()) - ts) > TOLERANCE_SECONDS:\n        raise ValueError(\"Timestamp outside the tolerance\")\n\n    key = base64.b64decode(secret.removeprefix(\"whsec_\"))\n    signed = f\"{msg_id}.{timestamp}.\".encode() + raw_body\n    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()\n\n    for entry in signatures.split(\" \"):\n        version, _, signature = entry.partition(\",\")\n        if version == \"v1\" and hmac.compare_digest(signature, expected):\n            return json.loads(raw_body)\n    raise ValueError(\"No matching signature\")\n","Python","python",[598,1807,1808,1813,1818,1823,1828,1833,1838,1842,1847,1851,1856,1861,1866,1871,1876,1881,1886,1890,1895,1900,1905,1910,1915,1920,1924,1929,1934,1939,1943,1948,1953,1958,1963],{"__ignoreMap":748},[752,1809,1810],{"class":754,"line":755},[752,1811,1812],{},"# Python 3.9 or later\n",[752,1814,1815],{"class":754,"line":1034},[752,1816,1817],{},"import base64\n",[752,1819,1820],{"class":754,"line":1041},[752,1821,1822],{},"import hashlib\n",[752,1824,1825],{"class":754,"line":1062},[752,1826,1827],{},"import hmac\n",[752,1829,1830],{"class":754,"line":1067},[752,1831,1832],{},"import json\n",[752,1834,1835],{"class":754,"line":1074},[752,1836,1837],{},"import time\n",[752,1839,1840],{"class":754,"line":1080},[752,1841,1038],{"emptyLinePlaceholder":1037},[752,1843,1844],{"class":754,"line":1116},[752,1845,1846],{},"TOLERANCE_SECONDS = 5 * 60\n",[752,1848,1849],{"class":754,"line":1144},[752,1850,1038],{"emptyLinePlaceholder":1037},[752,1852,1853],{"class":754,"line":1166},[752,1854,1855],{},"def verify_webhook(raw_body: bytes, headers, secret: str):\n",[752,1857,1858],{"class":754,"line":1188},[752,1859,1860],{},"    \"\"\"raw_body: the request body as bytes, before JSON parsing.\"\"\"\n",[752,1862,1863],{"class":754,"line":1242},[752,1864,1865],{},"    msg_id = headers.get(\"webhook-id\")\n",[752,1867,1868],{"class":754,"line":1247},[752,1869,1870],{},"    timestamp = headers.get(\"webhook-timestamp\")\n",[752,1872,1873],{"class":754,"line":1266},[752,1874,1875],{},"    signatures = headers.get(\"webhook-signature\")\n",[752,1877,1878],{"class":754,"line":1305},[752,1879,1880],{},"    if not msg_id or not timestamp or not signatures:\n",[752,1882,1883],{"class":754,"line":1361},[752,1884,1885],{},"        raise ValueError(\"Missing webhook headers\")\n",[752,1887,1888],{"class":754,"line":1382},[752,1889,1038],{"emptyLinePlaceholder":1037},[752,1891,1892],{"class":754,"line":1388},[752,1893,1894],{},"    try:\n",[752,1896,1897],{"class":754,"line":1393},[752,1898,1899],{},"        ts = int(timestamp)\n",[752,1901,1902],{"class":754,"line":1449},[752,1903,1904],{},"    except ValueError:\n",[752,1906,1907],{"class":754,"line":1462},[752,1908,1909],{},"        raise ValueError(\"Invalid timestamp\")\n",[752,1911,1912],{"class":754,"line":1486},[752,1913,1914],{},"    if abs(int(time.time()) - ts) > TOLERANCE_SECONDS:\n",[752,1916,1917],{"class":754,"line":1520},[752,1918,1919],{},"        raise ValueError(\"Timestamp outside the tolerance\")\n",[752,1921,1922],{"class":754,"line":1533},[752,1923,1038],{"emptyLinePlaceholder":1037},[752,1925,1926],{"class":754,"line":1544},[752,1927,1928],{},"    key = base64.b64decode(secret.removeprefix(\"whsec_\"))\n",[752,1930,1931],{"class":754,"line":1549},[752,1932,1933],{},"    signed = f\"{msg_id}.{timestamp}.\".encode() + raw_body\n",[752,1935,1936],{"class":754,"line":1583},[752,1937,1938],{},"    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()\n",[752,1940,1941],{"class":754,"line":1621},[752,1942,1038],{"emptyLinePlaceholder":1037},[752,1944,1945],{"class":754,"line":1652},[752,1946,1947],{},"    for entry in signatures.split(\" \"):\n",[752,1949,1950],{"class":754,"line":1682},[752,1951,1952],{},"        version, _, signature = entry.partition(\",\")\n",[752,1954,1955],{"class":754,"line":1729},[752,1956,1957],{},"        if version == \"v1\" and hmac.compare_digest(signature, expected):\n",[752,1959,1960],{"class":754,"line":1764},[752,1961,1962],{},"            return json.loads(raw_body)\n",[752,1964,1965],{"class":754,"line":1770},[752,1966,1967],{},"    raise ValueError(\"No matching signature\")\n",[743,1969,1974],{"className":1970,"code":1971,"filename":1972,"language":1973,"meta":748,"style":748},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003C?php\n\nconst TOLERANCE_SECONDS = 300;\n\n\u002F**\n * $rawBody: file_get_contents('php:\u002F\u002Finput'), before json_decode.\n * $headers: lower-case header names, e.g. from getallheaders() normalised.\n *\u002F\nfunction verifyWebhook(string $rawBody, array $headers, string $secret): array\n{\n    $id = $headers['webhook-id'] ?? '';\n    $timestamp = $headers['webhook-timestamp'] ?? '';\n    $signatures = $headers['webhook-signature'] ?? '';\n    if ($id === '' || $timestamp === '' || $signatures === '') {\n        throw new RuntimeException('Missing webhook headers');\n    }\n\n    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {\n        throw new RuntimeException('Timestamp outside the tolerance');\n    }\n\n    $key = base64_decode(preg_replace('\u002F^whsec_\u002F', '', $secret), true);\n    $expected = base64_encode(hash_hmac('sha256', \"{$id}.{$timestamp}.{$rawBody}\", $key, true));\n\n    foreach (explode(' ', $signatures) as $entry) {\n        [$version, $signature] = array_pad(explode(',', $entry, 2), 2, '');\n        if ($version === 'v1' && hash_equals($expected, $signature)) {\n            return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n        }\n    }\n    throw new RuntimeException('No matching signature');\n}\n","PHP","php",[598,1975,1976,1981,1985,1990,1994,1999,2004,2009,2014,2019,2023,2028,2033,2038,2043,2048,2052,2056,2061,2066,2070,2074,2079,2084,2088,2093,2098,2103,2108,2113,2117,2122],{"__ignoreMap":748},[752,1977,1978],{"class":754,"line":755},[752,1979,1980],{},"\u003C?php\n",[752,1982,1983],{"class":754,"line":1034},[752,1984,1038],{"emptyLinePlaceholder":1037},[752,1986,1987],{"class":754,"line":1041},[752,1988,1989],{},"const TOLERANCE_SECONDS = 300;\n",[752,1991,1992],{"class":754,"line":1062},[752,1993,1038],{"emptyLinePlaceholder":1037},[752,1995,1996],{"class":754,"line":1067},[752,1997,1998],{},"\u002F**\n",[752,2000,2001],{"class":754,"line":1074},[752,2002,2003],{}," * $rawBody: file_get_contents('php:\u002F\u002Finput'), before json_decode.\n",[752,2005,2006],{"class":754,"line":1080},[752,2007,2008],{}," * $headers: lower-case header names, e.g. from getallheaders() normalised.\n",[752,2010,2011],{"class":754,"line":1116},[752,2012,2013],{}," *\u002F\n",[752,2015,2016],{"class":754,"line":1144},[752,2017,2018],{},"function verifyWebhook(string $rawBody, array $headers, string $secret): array\n",[752,2020,2021],{"class":754,"line":1166},[752,2022,1358],{},[752,2024,2025],{"class":754,"line":1188},[752,2026,2027],{},"    $id = $headers['webhook-id'] ?? '';\n",[752,2029,2030],{"class":754,"line":1242},[752,2031,2032],{},"    $timestamp = $headers['webhook-timestamp'] ?? '';\n",[752,2034,2035],{"class":754,"line":1247},[752,2036,2037],{},"    $signatures = $headers['webhook-signature'] ?? '';\n",[752,2039,2040],{"class":754,"line":1266},[752,2041,2042],{},"    if ($id === '' || $timestamp === '' || $signatures === '') {\n",[752,2044,2045],{"class":754,"line":1305},[752,2046,2047],{},"        throw new RuntimeException('Missing webhook headers');\n",[752,2049,2050],{"class":754,"line":1361},[752,2051,1767],{},[752,2053,2054],{"class":754,"line":1382},[752,2055,1038],{"emptyLinePlaceholder":1037},[752,2057,2058],{"class":754,"line":1388},[752,2059,2060],{},"    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {\n",[752,2062,2063],{"class":754,"line":1393},[752,2064,2065],{},"        throw new RuntimeException('Timestamp outside the tolerance');\n",[752,2067,2068],{"class":754,"line":1449},[752,2069,1767],{},[752,2071,2072],{"class":754,"line":1462},[752,2073,1038],{"emptyLinePlaceholder":1037},[752,2075,2076],{"class":754,"line":1486},[752,2077,2078],{},"    $key = base64_decode(preg_replace('\u002F^whsec_\u002F', '', $secret), true);\n",[752,2080,2081],{"class":754,"line":1520},[752,2082,2083],{},"    $expected = base64_encode(hash_hmac('sha256', \"{$id}.{$timestamp}.{$rawBody}\", $key, true));\n",[752,2085,2086],{"class":754,"line":1533},[752,2087,1038],{"emptyLinePlaceholder":1037},[752,2089,2090],{"class":754,"line":1544},[752,2091,2092],{},"    foreach (explode(' ', $signatures) as $entry) {\n",[752,2094,2095],{"class":754,"line":1549},[752,2096,2097],{},"        [$version, $signature] = array_pad(explode(',', $entry, 2), 2, '');\n",[752,2099,2100],{"class":754,"line":1583},[752,2101,2102],{},"        if ($version === 'v1' && hash_equals($expected, $signature)) {\n",[752,2104,2105],{"class":754,"line":1621},[752,2106,2107],{},"            return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);\n",[752,2109,2110],{"class":754,"line":1652},[752,2111,2112],{},"        }\n",[752,2114,2115],{"class":754,"line":1682},[752,2116,1767],{},[752,2118,2119],{"class":754,"line":1729},[752,2120,2121],{},"    throw new RuntimeException('No matching signature');\n",[752,2123,2124],{"class":754,"line":1764},[752,2125,1799],{},[743,2127,2132],{"className":2128,"code":2129,"filename":2130,"language":2131,"meta":748,"style":748},"language-ruby shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Ruby 2.6 or later\nrequire \"base64\"\nrequire \"json\"\nrequire \"openssl\"\n\nTOLERANCE_SECONDS = 5 * 60\n\n# raw_body: request.raw_post (Rails) or request.body.read (Rack), before JSON parsing\ndef verify_webhook(raw_body, headers, secret)\n  id = headers[\"webhook-id\"]\n  timestamp = headers[\"webhook-timestamp\"]\n  signatures = headers[\"webhook-signature\"]\n  raise \"Missing webhook headers\" if [id, timestamp, signatures].any? { |v| v.nil? || v.empty? }\n\n  ts = Integer(timestamp, exception: false)\n  raise \"Timestamp outside the tolerance\" if ts.nil? || (Time.now.to_i - ts).abs > TOLERANCE_SECONDS\n\n  key = Base64.strict_decode64(secret.delete_prefix(\"whsec_\"))\n  digest = OpenSSL::HMAC.digest(\"SHA256\", key, \"#{id}.#{timestamp}.#{raw_body}\")\n  expected = Base64.strict_encode64(digest)\n\n  signatures.split(\" \").each do |entry|\n    version, signature = entry.split(\",\", 2)\n    next unless version == \"v1\" && signature\n    return JSON.parse(raw_body) if secure_compare(signature, expected)\n  end\n  raise \"No matching signature\"\nend\n\n# Constant-time string comparison.\ndef secure_compare(a, b)\n  return false unless a.bytesize == b.bytesize\n  result = 0\n  a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }\n  result.zero?\nend\n","Ruby","ruby",[598,2133,2134,2139,2144,2149,2154,2158,2162,2166,2171,2176,2181,2186,2191,2196,2200,2205,2210,2214,2219,2224,2229,2233,2238,2243,2248,2253,2258,2263,2268,2272,2277,2282,2287,2292,2297,2302],{"__ignoreMap":748},[752,2135,2136],{"class":754,"line":755},[752,2137,2138],{},"# Ruby 2.6 or later\n",[752,2140,2141],{"class":754,"line":1034},[752,2142,2143],{},"require \"base64\"\n",[752,2145,2146],{"class":754,"line":1041},[752,2147,2148],{},"require \"json\"\n",[752,2150,2151],{"class":754,"line":1062},[752,2152,2153],{},"require \"openssl\"\n",[752,2155,2156],{"class":754,"line":1067},[752,2157,1038],{"emptyLinePlaceholder":1037},[752,2159,2160],{"class":754,"line":1074},[752,2161,1846],{},[752,2163,2164],{"class":754,"line":1080},[752,2165,1038],{"emptyLinePlaceholder":1037},[752,2167,2168],{"class":754,"line":1116},[752,2169,2170],{},"# raw_body: request.raw_post (Rails) or request.body.read (Rack), before JSON parsing\n",[752,2172,2173],{"class":754,"line":1144},[752,2174,2175],{},"def verify_webhook(raw_body, headers, secret)\n",[752,2177,2178],{"class":754,"line":1166},[752,2179,2180],{},"  id = headers[\"webhook-id\"]\n",[752,2182,2183],{"class":754,"line":1188},[752,2184,2185],{},"  timestamp = headers[\"webhook-timestamp\"]\n",[752,2187,2188],{"class":754,"line":1242},[752,2189,2190],{},"  signatures = headers[\"webhook-signature\"]\n",[752,2192,2193],{"class":754,"line":1247},[752,2194,2195],{},"  raise \"Missing webhook headers\" if [id, timestamp, signatures].any? { |v| v.nil? || v.empty? }\n",[752,2197,2198],{"class":754,"line":1266},[752,2199,1038],{"emptyLinePlaceholder":1037},[752,2201,2202],{"class":754,"line":1305},[752,2203,2204],{},"  ts = Integer(timestamp, exception: false)\n",[752,2206,2207],{"class":754,"line":1361},[752,2208,2209],{},"  raise \"Timestamp outside the tolerance\" if ts.nil? || (Time.now.to_i - ts).abs > TOLERANCE_SECONDS\n",[752,2211,2212],{"class":754,"line":1382},[752,2213,1038],{"emptyLinePlaceholder":1037},[752,2215,2216],{"class":754,"line":1388},[752,2217,2218],{},"  key = Base64.strict_decode64(secret.delete_prefix(\"whsec_\"))\n",[752,2220,2221],{"class":754,"line":1393},[752,2222,2223],{},"  digest = OpenSSL::HMAC.digest(\"SHA256\", key, \"#{id}.#{timestamp}.#{raw_body}\")\n",[752,2225,2226],{"class":754,"line":1449},[752,2227,2228],{},"  expected = Base64.strict_encode64(digest)\n",[752,2230,2231],{"class":754,"line":1462},[752,2232,1038],{"emptyLinePlaceholder":1037},[752,2234,2235],{"class":754,"line":1486},[752,2236,2237],{},"  signatures.split(\" \").each do |entry|\n",[752,2239,2240],{"class":754,"line":1520},[752,2241,2242],{},"    version, signature = entry.split(\",\", 2)\n",[752,2244,2245],{"class":754,"line":1533},[752,2246,2247],{},"    next unless version == \"v1\" && signature\n",[752,2249,2250],{"class":754,"line":1544},[752,2251,2252],{},"    return JSON.parse(raw_body) if secure_compare(signature, expected)\n",[752,2254,2255],{"class":754,"line":1549},[752,2256,2257],{},"  end\n",[752,2259,2260],{"class":754,"line":1583},[752,2261,2262],{},"  raise \"No matching signature\"\n",[752,2264,2265],{"class":754,"line":1621},[752,2266,2267],{},"end\n",[752,2269,2270],{"class":754,"line":1652},[752,2271,1038],{"emptyLinePlaceholder":1037},[752,2273,2274],{"class":754,"line":1682},[752,2275,2276],{},"# Constant-time string comparison.\n",[752,2278,2279],{"class":754,"line":1729},[752,2280,2281],{},"def secure_compare(a, b)\n",[752,2283,2284],{"class":754,"line":1764},[752,2285,2286],{},"  return false unless a.bytesize == b.bytesize\n",[752,2288,2289],{"class":754,"line":1770},[752,2290,2291],{},"  result = 0\n",[752,2293,2294],{"class":754,"line":1775},[752,2295,2296],{},"  a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }\n",[752,2298,2299],{"class":754,"line":1796},[752,2300,2301],{},"  result.zero?\n",[752,2303,2305],{"class":754,"line":2304},36,[752,2306,2267],{},[743,2308,2313],{"className":2309,"code":2310,"filename":2311,"language":2312,"meta":748,"style":748},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","package webhooks\n\nimport (\n    \"crypto\u002Fhmac\"\n    \"crypto\u002Fsha256\"\n    \"encoding\u002Fbase64\"\n    \"errors\"\n    \"net\u002Fhttp\"\n    \"strconv\"\n    \"strings\"\n    \"time\"\n)\n\nconst toleranceSeconds = 5 * 60\n\n\u002F\u002F rawBody: the request body bytes, read before any JSON decoding.\nfunc VerifyWebhook(rawBody []byte, header http.Header, secret string) error {\n    id := header.Get(\"webhook-id\")\n    timestamp := header.Get(\"webhook-timestamp\")\n    signatures := header.Get(\"webhook-signature\")\n    if id == \"\" || timestamp == \"\" || signatures == \"\" {\n        return errors.New(\"missing webhook headers\")\n    }\n\n    ts, err := strconv.ParseInt(timestamp, 10, 64)\n    if err != nil {\n        return errors.New(\"invalid timestamp\")\n    }\n    if diff := time.Now().Unix() - ts; diff > toleranceSeconds || diff \u003C -toleranceSeconds {\n        return errors.New(\"timestamp outside the tolerance\")\n    }\n\n    key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, \"whsec_\"))\n    if err != nil {\n        return errors.New(\"invalid secret\")\n    }\n    mac := hmac.New(sha256.New, key)\n    mac.Write([]byte(id + \".\" + timestamp + \".\"))\n    mac.Write(rawBody)\n    expected := mac.Sum(nil)\n\n    for _, entry := range strings.Split(signatures, \" \") {\n        version, signature, found := strings.Cut(entry, \",\")\n        if !found || version != \"v1\" {\n            continue\n        }\n        given, err := base64.StdEncoding.DecodeString(signature)\n        if err == nil && hmac.Equal(given, expected) {\n            return nil\n        }\n    }\n    return errors.New(\"no matching signature\")\n}\n","Go","go",[598,2314,2315,2320,2324,2329,2334,2339,2344,2349,2354,2359,2364,2369,2373,2377,2382,2386,2391,2396,2401,2406,2411,2416,2421,2425,2429,2434,2439,2444,2448,2453,2458,2462,2466,2471,2475,2480,2484,2490,2496,2502,2508,2513,2519,2525,2531,2537,2542,2548,2554,2560,2565,2570,2576],{"__ignoreMap":748},[752,2316,2317],{"class":754,"line":755},[752,2318,2319],{},"package webhooks\n",[752,2321,2322],{"class":754,"line":1034},[752,2323,1038],{"emptyLinePlaceholder":1037},[752,2325,2326],{"class":754,"line":1041},[752,2327,2328],{},"import (\n",[752,2330,2331],{"class":754,"line":1062},[752,2332,2333],{},"    \"crypto\u002Fhmac\"\n",[752,2335,2336],{"class":754,"line":1067},[752,2337,2338],{},"    \"crypto\u002Fsha256\"\n",[752,2340,2341],{"class":754,"line":1074},[752,2342,2343],{},"    \"encoding\u002Fbase64\"\n",[752,2345,2346],{"class":754,"line":1080},[752,2347,2348],{},"    \"errors\"\n",[752,2350,2351],{"class":754,"line":1116},[752,2352,2353],{},"    \"net\u002Fhttp\"\n",[752,2355,2356],{"class":754,"line":1144},[752,2357,2358],{},"    \"strconv\"\n",[752,2360,2361],{"class":754,"line":1166},[752,2362,2363],{},"    \"strings\"\n",[752,2365,2366],{"class":754,"line":1188},[752,2367,2368],{},"    \"time\"\n",[752,2370,2371],{"class":754,"line":1242},[752,2372,1239],{},[752,2374,2375],{"class":754,"line":1247},[752,2376,1038],{"emptyLinePlaceholder":1037},[752,2378,2379],{"class":754,"line":1266},[752,2380,2381],{},"const toleranceSeconds = 5 * 60\n",[752,2383,2384],{"class":754,"line":1305},[752,2385,1038],{"emptyLinePlaceholder":1037},[752,2387,2388],{"class":754,"line":1361},[752,2389,2390],{},"\u002F\u002F rawBody: the request body bytes, read before any JSON decoding.\n",[752,2392,2393],{"class":754,"line":1382},[752,2394,2395],{},"func VerifyWebhook(rawBody []byte, header http.Header, secret string) error {\n",[752,2397,2398],{"class":754,"line":1388},[752,2399,2400],{},"    id := header.Get(\"webhook-id\")\n",[752,2402,2403],{"class":754,"line":1393},[752,2404,2405],{},"    timestamp := header.Get(\"webhook-timestamp\")\n",[752,2407,2408],{"class":754,"line":1449},[752,2409,2410],{},"    signatures := header.Get(\"webhook-signature\")\n",[752,2412,2413],{"class":754,"line":1462},[752,2414,2415],{},"    if id == \"\" || timestamp == \"\" || signatures == \"\" {\n",[752,2417,2418],{"class":754,"line":1486},[752,2419,2420],{},"        return errors.New(\"missing webhook headers\")\n",[752,2422,2423],{"class":754,"line":1520},[752,2424,1767],{},[752,2426,2427],{"class":754,"line":1533},[752,2428,1038],{"emptyLinePlaceholder":1037},[752,2430,2431],{"class":754,"line":1544},[752,2432,2433],{},"    ts, err := strconv.ParseInt(timestamp, 10, 64)\n",[752,2435,2436],{"class":754,"line":1549},[752,2437,2438],{},"    if err != nil {\n",[752,2440,2441],{"class":754,"line":1583},[752,2442,2443],{},"        return errors.New(\"invalid timestamp\")\n",[752,2445,2446],{"class":754,"line":1621},[752,2447,1767],{},[752,2449,2450],{"class":754,"line":1652},[752,2451,2452],{},"    if diff := time.Now().Unix() - ts; diff > toleranceSeconds || diff \u003C -toleranceSeconds {\n",[752,2454,2455],{"class":754,"line":1682},[752,2456,2457],{},"        return errors.New(\"timestamp outside the tolerance\")\n",[752,2459,2460],{"class":754,"line":1729},[752,2461,1767],{},[752,2463,2464],{"class":754,"line":1764},[752,2465,1038],{"emptyLinePlaceholder":1037},[752,2467,2468],{"class":754,"line":1770},[752,2469,2470],{},"    key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, \"whsec_\"))\n",[752,2472,2473],{"class":754,"line":1775},[752,2474,2438],{},[752,2476,2477],{"class":754,"line":1796},[752,2478,2479],{},"        return errors.New(\"invalid secret\")\n",[752,2481,2482],{"class":754,"line":2304},[752,2483,1767],{},[752,2485,2487],{"class":754,"line":2486},37,[752,2488,2489],{},"    mac := hmac.New(sha256.New, key)\n",[752,2491,2493],{"class":754,"line":2492},38,[752,2494,2495],{},"    mac.Write([]byte(id + \".\" + timestamp + \".\"))\n",[752,2497,2499],{"class":754,"line":2498},39,[752,2500,2501],{},"    mac.Write(rawBody)\n",[752,2503,2505],{"class":754,"line":2504},40,[752,2506,2507],{},"    expected := mac.Sum(nil)\n",[752,2509,2511],{"class":754,"line":2510},41,[752,2512,1038],{"emptyLinePlaceholder":1037},[752,2514,2516],{"class":754,"line":2515},42,[752,2517,2518],{},"    for _, entry := range strings.Split(signatures, \" \") {\n",[752,2520,2522],{"class":754,"line":2521},43,[752,2523,2524],{},"        version, signature, found := strings.Cut(entry, \",\")\n",[752,2526,2528],{"class":754,"line":2527},44,[752,2529,2530],{},"        if !found || version != \"v1\" {\n",[752,2532,2534],{"class":754,"line":2533},45,[752,2535,2536],{},"            continue\n",[752,2538,2540],{"class":754,"line":2539},46,[752,2541,2112],{},[752,2543,2545],{"class":754,"line":2544},47,[752,2546,2547],{},"        given, err := base64.StdEncoding.DecodeString(signature)\n",[752,2549,2551],{"class":754,"line":2550},48,[752,2552,2553],{},"        if err == nil && hmac.Equal(given, expected) {\n",[752,2555,2557],{"class":754,"line":2556},49,[752,2558,2559],{},"            return nil\n",[752,2561,2563],{"class":754,"line":2562},50,[752,2564,2112],{},[752,2566,2568],{"class":754,"line":2567},51,[752,2569,1767],{},[752,2571,2573],{"class":754,"line":2572},52,[752,2574,2575],{},"    return errors.New(\"no matching signature\")\n",[752,2577,2579],{"class":754,"line":2578},53,[752,2580,1799],{},[743,2582,2587],{"className":2583,"code":2584,"filename":2585,"language":2586,"meta":748,"style":748},"language-csharp shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","using System;\nusing System.Security.Cryptography;\nusing System.Text;\n\npublic static class WebhookVerifier\n{\n    private const long ToleranceSeconds = 5 * 60;\n\n    \u002F\u002F rawBody: the request body bytes, read before any JSON deserialisation.\n    public static void Verify(byte[] rawBody, string id, string timestamp, string signatures, string secret)\n    {\n        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signatures))\n            throw new UnauthorizedAccessException(\"Missing webhook headers\");\n\n        if (!long.TryParse(timestamp, out var ts) ||\n            Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > ToleranceSeconds)\n            throw new UnauthorizedAccessException(\"Timestamp outside the tolerance\");\n\n        var key = Convert.FromBase64String(secret.StartsWith(\"whsec_\") ? secret[6..] : secret);\n        var prefix = Encoding.UTF8.GetBytes($\"{id}.{timestamp}.\");\n        var signed = new byte[prefix.Length + rawBody.Length];\n        Buffer.BlockCopy(prefix, 0, signed, 0, prefix.Length);\n        Buffer.BlockCopy(rawBody, 0, signed, prefix.Length, rawBody.Length);\n\n        using var hmac = new HMACSHA256(key);\n        var expected = hmac.ComputeHash(signed);\n\n        foreach (var entry in signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries))\n        {\n            var parts = entry.Split(',', 2);\n            if (parts.Length != 2 || parts[0] != \"v1\") continue;\n            byte[] given;\n            try { given = Convert.FromBase64String(parts[1]); } catch (FormatException) { continue; }\n            if (CryptographicOperations.FixedTimeEquals(given, expected)) return;\n        }\n        throw new UnauthorizedAccessException(\"No matching signature\");\n    }\n}\n","C#","csharp",[598,2588,2589,2594,2599,2604,2608,2613,2617,2622,2626,2631,2636,2641,2646,2651,2655,2660,2665,2670,2674,2679,2684,2689,2694,2699,2703,2708,2713,2717,2722,2727,2732,2737,2742,2747,2752,2756,2761,2765],{"__ignoreMap":748},[752,2590,2591],{"class":754,"line":755},[752,2592,2593],{},"using System;\n",[752,2595,2596],{"class":754,"line":1034},[752,2597,2598],{},"using System.Security.Cryptography;\n",[752,2600,2601],{"class":754,"line":1041},[752,2602,2603],{},"using System.Text;\n",[752,2605,2606],{"class":754,"line":1062},[752,2607,1038],{"emptyLinePlaceholder":1037},[752,2609,2610],{"class":754,"line":1067},[752,2611,2612],{},"public static class WebhookVerifier\n",[752,2614,2615],{"class":754,"line":1074},[752,2616,1358],{},[752,2618,2619],{"class":754,"line":1080},[752,2620,2621],{},"    private const long ToleranceSeconds = 5 * 60;\n",[752,2623,2624],{"class":754,"line":1116},[752,2625,1038],{"emptyLinePlaceholder":1037},[752,2627,2628],{"class":754,"line":1144},[752,2629,2630],{},"    \u002F\u002F rawBody: the request body bytes, read before any JSON deserialisation.\n",[752,2632,2633],{"class":754,"line":1166},[752,2634,2635],{},"    public static void Verify(byte[] rawBody, string id, string timestamp, string signatures, string secret)\n",[752,2637,2638],{"class":754,"line":1188},[752,2639,2640],{},"    {\n",[752,2642,2643],{"class":754,"line":1242},[752,2644,2645],{},"        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signatures))\n",[752,2647,2648],{"class":754,"line":1247},[752,2649,2650],{},"            throw new UnauthorizedAccessException(\"Missing webhook headers\");\n",[752,2652,2653],{"class":754,"line":1266},[752,2654,1038],{"emptyLinePlaceholder":1037},[752,2656,2657],{"class":754,"line":1305},[752,2658,2659],{},"        if (!long.TryParse(timestamp, out var ts) ||\n",[752,2661,2662],{"class":754,"line":1361},[752,2663,2664],{},"            Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > ToleranceSeconds)\n",[752,2666,2667],{"class":754,"line":1382},[752,2668,2669],{},"            throw new UnauthorizedAccessException(\"Timestamp outside the tolerance\");\n",[752,2671,2672],{"class":754,"line":1388},[752,2673,1038],{"emptyLinePlaceholder":1037},[752,2675,2676],{"class":754,"line":1393},[752,2677,2678],{},"        var key = Convert.FromBase64String(secret.StartsWith(\"whsec_\") ? secret[6..] : secret);\n",[752,2680,2681],{"class":754,"line":1449},[752,2682,2683],{},"        var prefix = Encoding.UTF8.GetBytes($\"{id}.{timestamp}.\");\n",[752,2685,2686],{"class":754,"line":1462},[752,2687,2688],{},"        var signed = new byte[prefix.Length + rawBody.Length];\n",[752,2690,2691],{"class":754,"line":1486},[752,2692,2693],{},"        Buffer.BlockCopy(prefix, 0, signed, 0, prefix.Length);\n",[752,2695,2696],{"class":754,"line":1520},[752,2697,2698],{},"        Buffer.BlockCopy(rawBody, 0, signed, prefix.Length, rawBody.Length);\n",[752,2700,2701],{"class":754,"line":1533},[752,2702,1038],{"emptyLinePlaceholder":1037},[752,2704,2705],{"class":754,"line":1544},[752,2706,2707],{},"        using var hmac = new HMACSHA256(key);\n",[752,2709,2710],{"class":754,"line":1549},[752,2711,2712],{},"        var expected = hmac.ComputeHash(signed);\n",[752,2714,2715],{"class":754,"line":1583},[752,2716,1038],{"emptyLinePlaceholder":1037},[752,2718,2719],{"class":754,"line":1621},[752,2720,2721],{},"        foreach (var entry in signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries))\n",[752,2723,2724],{"class":754,"line":1652},[752,2725,2726],{},"        {\n",[752,2728,2729],{"class":754,"line":1682},[752,2730,2731],{},"            var parts = entry.Split(',', 2);\n",[752,2733,2734],{"class":754,"line":1729},[752,2735,2736],{},"            if (parts.Length != 2 || parts[0] != \"v1\") continue;\n",[752,2738,2739],{"class":754,"line":1764},[752,2740,2741],{},"            byte[] given;\n",[752,2743,2744],{"class":754,"line":1770},[752,2745,2746],{},"            try { given = Convert.FromBase64String(parts[1]); } catch (FormatException) { continue; }\n",[752,2748,2749],{"class":754,"line":1775},[752,2750,2751],{},"            if (CryptographicOperations.FixedTimeEquals(given, expected)) return;\n",[752,2753,2754],{"class":754,"line":1796},[752,2755,2112],{},[752,2757,2758],{"class":754,"line":2304},[752,2759,2760],{},"        throw new UnauthorizedAccessException(\"No matching signature\");\n",[752,2762,2763],{"class":754,"line":2486},[752,2764,1767],{},[752,2766,2767],{"class":754,"line":2492},[752,2768,1799],{},[569,2770,2772],{"id":2771},"rotating-the-signing-secret","Rotating the signing secret",[553,2774,2775],{},"Rotate the secret when it may have been exposed, when someone who knew it leaves, or as routine hygiene.",[647,2777,2778,2792,2798],{},[650,2779,2780,2781,2784,2785,2787,2788,2791],{},"In ",[661,2782,2783],{},"Settings > API",", open the ",[661,2786,209],{}," tab and choose ",[661,2789,2790],{},"Rotate secret"," in the endpoint's menu. You need to own the endpoint or have a role above its owner's.",[650,2793,2794,2795,641],{},"Copy the new secret. It is shown ",[661,2796,2797],{},"once",[650,2799,2800],{},"Deploy it to your receiver within 24 hours.",[553,2802,2803,2804,2807],{},"For ",[661,2805,2806],{},"24 hours"," after a rotation the old secret keeps signing too: every delivery carries two signatures, the new secret's first and the old secret's second. Because your code accepts any matching signature, no delivery is rejected while you switch. After 24 hours only the new secret signs.",[553,2809,2810],{},"Rotating again within those 24 hours ends the overlap for the oldest secret: only the secret being replaced keeps signing alongside the new one.",[569,2812,288],{"id":2813},"troubleshooting",[574,2815,2816,2826],{},[577,2817,2818],{},[580,2819,2820,2823],{},[583,2821,2822],{},"Symptom",[583,2824,2825],{},"Likely cause",[590,2827,2828,2839,2847,2855,2863],{},[580,2829,2830,2833],{},[595,2831,2832],{},"Signature never matches",[595,2834,2835,2836,2838],{},"The body was parsed and re-serialized before verifying, or the secret was copied with extra whitespace or without the ",[598,2837,655],{}," prefix handling",[580,2840,2841,2844],{},[595,2842,2843],{},"Only some deliveries fail",[595,2845,2846],{},"A proxy or framework rewrites some bodies (for example, normalising line endings or character encoding)",[580,2848,2849,2852],{},[595,2850,2851],{},"Fails with \"timestamp outside the tolerance\"",[595,2853,2854],{},"Your server's clock is wrong. Enable time synchronisation (NTP)",[580,2856,2857,2860],{},[595,2858,2859],{},"Fails after rotating",[595,2861,2862],{},"The old secret is still deployed after its 24-hour overlap ended",[580,2864,2865,2868],{},[595,2866,2867],{},"Fails only on retries",[595,2869,2870],{},"The timestamp was checked against the first attempt's time. Each attempt is signed with its own timestamp; verify every attempt as it arrives",[2872,2873,2874],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":748,"searchDepth":755,"depth":1034,"links":2876},[2877,2878,2881,2882,2883,2884],{"id":571,"depth":1034,"text":572},{"id":644,"depth":1034,"text":645,"children":2879},[2880],{"id":683,"depth":1041,"text":684},{"id":942,"depth":1034,"text":943},{"id":994,"depth":1034,"text":995},{"id":2771,"depth":1034,"text":2772},{"id":2813,"depth":1034,"text":288},"Check that a webhook really came from BookYourPTO and was not replayed, with verification samples in Node.js, Python, PHP, Ruby, Go and C#.","md",null,{},{"icon":227},{"title":224,"description":2885},"5krGVicf1T5K5XedyGr7TqpVtpr8lFNGhzSEFq6jbXM",[2893,2895],{"title":219,"path":220,"stem":221,"description":2894,"icon":222,"children":-1},"Every webhook event type, when it fires, the read scope its endpoint's owner needs, and a complete example payload for each.",{"title":229,"path":230,"stem":231,"description":2896,"icon":232,"children":-1},"What counts as a successful webhook delivery, the retry schedule, when an endpoint is turned off automatically, and how to redeliver events.",1791906547120]