[{"data":1,"prerenderedAt":589},["ShallowReactive",2],{"docs-\u002Fdocs\u002Fwebhooks\u002Fsignatures":3,"docs-section-Webhooks":584},{"id":4,"title":5,"body":6,"description":577,"extension":578,"meta":579,"navigation":131,"order":135,"path":580,"section":564,"seo":581,"stem":582,"__hash__":583},"docs\u002Fdocs\u002Fwebhooks\u002Fsignatures.md","Check the signature",{"type":7,"value":8,"toc":570},"minimark",[9,13,18,30,55,58,87,90,94,442,446,512,516,551,555,566],[10,11,12],"p",{},"Anyone who learns your address could send it a request. Every request we send is signed with your webhook's secret, so you can tell ours from theirs. Check it before you trust what a request says.",[14,15,17],"h2",{"id":16},"how-its-signed","How it's signed",[10,19,20,21,25,26,29],{},"The ",[22,23,24],"code",{},"Webhook-Signature"," header looks like ",[22,27,28],{},"t=1793620800,v1=5257a8…",":",[31,32,33,40],"ul",{},[34,35,36,39],"li",{},[22,37,38],{},"t"," is the time we sent it, in seconds.",[34,41,42,45,46,49,50,54],{},[22,43,44],{},"v1"," is the hex of an HMAC-SHA256, with your secret as the key, of the text ",[22,47,48],{},"\u003Ct>.\u003Cbody>",": the time, a dot, then the ",[51,52,53],"strong",{},"raw body exactly as it arrived",".",[10,56,57],{},"To check a request:",[59,60,61,69,75,81],"ol",{},[34,62,63,64,66,67,54],{},"Split the header into ",[22,65,38],{}," and ",[22,68,44],{},[34,70,71,72,74],{},"Compute the HMAC of ",[22,73,38],{},", a dot and the raw body.",[34,76,77,78,80],{},"Compare it to ",[22,79,44],{}," in constant time.",[34,82,83,84,86],{},"Refuse the request if ",[22,85,38],{}," is more than 300 seconds from your clock. That stops someone sending an old request again.",[10,88,89],{},"Use the raw body, not JSON that your framework has parsed and written out again: a different spacing or order of keys gives a different signature.",[14,91,93],{"id":92},"node","Node",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-js shiki shiki-themes github-light github-dark","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nexport function verify(secret, header, rawBody, toleranceSeconds = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(Date.now() \u002F 1000 - t) > toleranceSeconds) return false;\n  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');\n  const given = parts.v1 ?? '';\n  return expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));\n}\n","js","",[22,102,103,126,133,179,243,259,324,376,397,436],{"__ignoreMap":100},[104,105,108,112,116,119,123],"span",{"class":106,"line":107},"line",1,[104,109,111],{"class":110},"szBVR","import",[104,113,115],{"class":114},"sVt8B"," { createHmac, timingSafeEqual } ",[104,117,118],{"class":110},"from",[104,120,122],{"class":121},"sZZnC"," 'node:crypto'",[104,124,125],{"class":114},";\n",[104,127,129],{"class":106,"line":128},2,[104,130,132],{"emptyLinePlaceholder":131},true,"\n",[104,134,136,139,142,146,149,153,156,159,161,164,166,169,172,176],{"class":106,"line":135},3,[104,137,138],{"class":110},"export",[104,140,141],{"class":110}," function",[104,143,145],{"class":144},"sScJk"," verify",[104,147,148],{"class":114},"(",[104,150,152],{"class":151},"s4XuR","secret",[104,154,155],{"class":114},", ",[104,157,158],{"class":151},"header",[104,160,155],{"class":114},[104,162,163],{"class":151},"rawBody",[104,165,155],{"class":114},[104,167,168],{"class":151},"toleranceSeconds",[104,170,171],{"class":110}," =",[104,173,175],{"class":174},"sj4cs"," 300",[104,177,178],{"class":114},") {\n",[104,180,182,185,188,190,193,196,199,202,204,207,210,213,216,218,221,224,227,230,233,235,237,240],{"class":106,"line":181},4,[104,183,184],{"class":110},"  const",[104,186,187],{"class":174}," parts",[104,189,171],{"class":110},[104,191,192],{"class":114}," Object.",[104,194,195],{"class":144},"fromEntries",[104,197,198],{"class":114},"(header.",[104,200,201],{"class":144},"split",[104,203,148],{"class":114},[104,205,206],{"class":121},"','",[104,208,209],{"class":114},").",[104,211,212],{"class":144},"map",[104,214,215],{"class":114},"((",[104,217,10],{"class":151},[104,219,220],{"class":114},") ",[104,222,223],{"class":110},"=>",[104,225,226],{"class":114}," p.",[104,228,229],{"class":144},"trim",[104,231,232],{"class":114},"().",[104,234,201],{"class":144},[104,236,148],{"class":114},[104,238,239],{"class":121},"'='",[104,241,242],{"class":114},")));\n",[104,244,246,248,251,253,256],{"class":106,"line":245},5,[104,247,184],{"class":110},[104,249,250],{"class":174}," t",[104,252,171],{"class":110},[104,254,255],{"class":144}," Number",[104,257,258],{"class":114},"(parts.t);\n",[104,260,262,265,268,271,274,277,280,283,286,289,292,295,298,301,304,307,310,313,316,319,322],{"class":106,"line":261},6,[104,263,264],{"class":110},"  if",[104,266,267],{"class":114}," (",[104,269,270],{"class":110},"!",[104,272,273],{"class":114},"Number.",[104,275,276],{"class":144},"isInteger",[104,278,279],{"class":114},"(t) ",[104,281,282],{"class":110},"||",[104,284,285],{"class":114}," Math.",[104,287,288],{"class":144},"abs",[104,290,291],{"class":114},"(Date.",[104,293,294],{"class":144},"now",[104,296,297],{"class":114},"() ",[104,299,300],{"class":110},"\u002F",[104,302,303],{"class":174}," 1000",[104,305,306],{"class":110}," -",[104,308,309],{"class":114}," t) ",[104,311,312],{"class":110},">",[104,314,315],{"class":114}," toleranceSeconds) ",[104,317,318],{"class":110},"return",[104,320,321],{"class":174}," false",[104,323,125],{"class":114},[104,325,327,329,332,334,337,339,342,345,348,350,353,355,358,360,363,365,368,370,373],{"class":106,"line":326},7,[104,328,184],{"class":110},[104,330,331],{"class":174}," expected",[104,333,171],{"class":110},[104,335,336],{"class":144}," createHmac",[104,338,148],{"class":114},[104,340,341],{"class":121},"'sha256'",[104,343,344],{"class":114},", secret).",[104,346,347],{"class":144},"update",[104,349,148],{"class":114},[104,351,352],{"class":121},"`${",[104,354,38],{"class":114},[104,356,357],{"class":121},"}.${",[104,359,163],{"class":114},[104,361,362],{"class":121},"}`",[104,364,209],{"class":114},[104,366,367],{"class":144},"digest",[104,369,148],{"class":114},[104,371,372],{"class":121},"'hex'",[104,374,375],{"class":114},");\n",[104,377,379,381,384,386,389,392,395],{"class":106,"line":378},8,[104,380,184],{"class":110},[104,382,383],{"class":174}," given",[104,385,171],{"class":110},[104,387,388],{"class":114}," parts.v1 ",[104,390,391],{"class":110},"??",[104,393,394],{"class":121}," ''",[104,396,125],{"class":114},[104,398,400,403,406,409,412,415,417,420,423,426,428,431,433],{"class":106,"line":399},9,[104,401,402],{"class":110},"  return",[104,404,405],{"class":114}," expected.",[104,407,408],{"class":174},"length",[104,410,411],{"class":110}," ===",[104,413,414],{"class":114}," given.",[104,416,408],{"class":174},[104,418,419],{"class":110}," &&",[104,421,422],{"class":144}," timingSafeEqual",[104,424,425],{"class":114},"(Buffer.",[104,427,118],{"class":144},[104,429,430],{"class":114},"(expected), Buffer.",[104,432,118],{"class":144},[104,434,435],{"class":114},"(given));\n",[104,437,439],{"class":106,"line":438},10,[104,440,441],{"class":114},"}\n",[14,443,445],{"id":444},"python","Python",[95,447,450],{"className":448,"code":449,"language":444,"meta":100,"style":100},"language-python shiki shiki-themes github-light github-dark","import hashlib, hmac, time\n\ndef verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:\n    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\"))\n    try:\n        t = int(parts[\"t\"])\n    except (KeyError, ValueError):\n        return False\n    if abs(time.time() - t) > tolerance:\n        return False\n    expected = hmac.new(secret.encode(), f\"{t}.\".encode() + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n",[22,451,452,457,461,466,471,476,481,486,491,496,500,506],{"__ignoreMap":100},[104,453,454],{"class":106,"line":107},[104,455,456],{},"import hashlib, hmac, time\n",[104,458,459],{"class":106,"line":128},[104,460,132],{"emptyLinePlaceholder":131},[104,462,463],{"class":106,"line":135},[104,464,465],{},"def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:\n",[104,467,468],{"class":106,"line":181},[104,469,470],{},"    parts = dict(p.strip().split(\"=\", 1) for p in header.split(\",\"))\n",[104,472,473],{"class":106,"line":245},[104,474,475],{},"    try:\n",[104,477,478],{"class":106,"line":261},[104,479,480],{},"        t = int(parts[\"t\"])\n",[104,482,483],{"class":106,"line":326},[104,484,485],{},"    except (KeyError, ValueError):\n",[104,487,488],{"class":106,"line":378},[104,489,490],{},"        return False\n",[104,492,493],{"class":106,"line":399},[104,494,495],{},"    if abs(time.time() - t) > tolerance:\n",[104,497,498],{"class":106,"line":438},[104,499,490],{},[104,501,503],{"class":106,"line":502},11,[104,504,505],{},"    expected = hmac.new(secret.encode(), f\"{t}.\".encode() + raw_body, hashlib.sha256).hexdigest()\n",[104,507,509],{"class":106,"line":508},12,[104,510,511],{},"    return hmac.compare_digest(expected, parts.get(\"v1\", \"\"))\n",[14,513,515],{"id":514},"php","PHP",[95,517,520],{"className":518,"code":519,"language":514,"meta":100,"style":100},"language-php shiki shiki-themes github-light github-dark","function verify(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {\n    parse_str(str_replace(',', '&', $header), $parts);\n    if (!isset($parts['t'], $parts['v1']) || abs(time() - (int) $parts['t']) > $tolerance) return false;\n    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);\n    return hash_equals($expected, $parts['v1']);\n}\n",[22,521,522,527,532,537,542,547],{"__ignoreMap":100},[104,523,524],{"class":106,"line":107},[104,525,526],{},"function verify(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {\n",[104,528,529],{"class":106,"line":128},[104,530,531],{},"    parse_str(str_replace(',', '&', $header), $parts);\n",[104,533,534],{"class":106,"line":135},[104,535,536],{},"    if (!isset($parts['t'], $parts['v1']) || abs(time() - (int) $parts['t']) > $tolerance) return false;\n",[104,538,539],{"class":106,"line":181},[104,540,541],{},"    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);\n",[104,543,544],{"class":106,"line":245},[104,545,546],{},"    return hash_equals($expected, $parts['v1']);\n",[104,548,549],{"class":106,"line":261},[104,550,441],{},[14,552,554],{"id":553},"keep-the-secret-safe","Keep the secret safe",[10,556,557,558,561,562,565],{},"The secret starts with ",[22,559,560],{},"whsec_",". Keep it on your server, never in a web page or a shared document. If it leaks, delete the webhook in ",[51,563,564],{},"Webhooks"," and make a new one: the new one has a new secret.",[567,568,569],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}",{"title":100,"searchDepth":128,"depth":128,"links":571},[572,573,574,575,576],{"id":16,"depth":128,"text":17},{"id":92,"depth":128,"text":93},{"id":444,"depth":128,"text":445},{"id":514,"depth":128,"text":515},{"id":553,"depth":128,"text":554},"How to be sure a request came from us and wasn't changed, with code in Node, Python and PHP.","md",{},"\u002Fdocs\u002Fwebhooks\u002Fsignatures",{"title":5,"description":577},"docs\u002Fwebhooks\u002Fsignatures","RBfwLoQlGq3Na-4p8J4VOK7dLNGybEQee41lUNmM3Wo",[585,588],{"path":586,"title":587},"\u002Fdocs\u002Fwebhooks\u002Fevents","Events",{"path":580,"title":5},1793740287733]