[{"data":1,"prerenderedAt":2380},["ShallowReactive",2],{"navigation":3,"/api/webhooks/verifying-signatures":258,"/api/webhooks/verifying-signatures-surround":2375},[4,22,63,85,115,131,219,245],{"title":5,"path":6,"stem":7,"children":8,"page":21},"Getting Started","/getting-started","1.getting-started",[9,13,17],{"title":10,"path":11,"stem":12},"Introduction","/getting-started/introduction","1.getting-started/1.introduction",{"title":14,"path":15,"stem":16},"Quick Start","/getting-started/quick-start","1.getting-started/2.quick-start",{"title":18,"path":19,"stem":20},"Key Concepts","/getting-started/key-concepts","1.getting-started/3.key-concepts",false,{"title":23,"path":24,"stem":25,"children":26,"page":21},"Guides","/guides","2.guides",[27,31,35,39,43,47,51,55,59],{"title":28,"path":29,"stem":30},"Quick Share Guide","/guides/quick-share","2.guides/1.quick-share",{"title":32,"path":33,"stem":34},"Slack Integration","/guides/slack-integration","2.guides/2.slack-integration",{"title":36,"path":37,"stem":38},"Enterprise Setup","/guides/enterprise-setup","2.guides/3.enterprise-setup",{"title":40,"path":41,"stem":42},"Creating a Share","/guides/creating-a-share","2.guides/4.creating-a-share",{"title":44,"path":45,"stem":46},"Zero-Knowledge Custody","/guides/zero-knowledge-custody","2.guides/5.zero-knowledge-custody",{"title":48,"path":49,"stem":50},"Secure Requests","/guides/secure-requests","2.guides/6.secure-requests",{"title":52,"path":53,"stem":54},"Receiving a Secure Request","/guides/receiving-a-secure-request","2.guides/7.receiving-a-secure-request",{"title":56,"path":57,"stem":58},"SecurePaste","/guides/securepaste","2.guides/8.securepaste",{"title":60,"path":61,"stem":62},"Notifications","/guides/notifications","2.guides/9.notifications",{"title":64,"icon":65,"path":66,"stem":67,"children":68,"page":21},"Account","i-lucide-user-cog","/account","3.account",[69,73,77,81],{"title":70,"path":71,"stem":72},"Signing In","/account/signing-in","3.account/1.signing-in",{"title":74,"path":75,"stem":76},"Plans and Share Allowance","/account/plans-and-share-allowance","3.account/2.plans-and-share-allowance",{"title":78,"path":79,"stem":80},"Billing and Subscription","/account/billing-and-subscription","3.account/3.billing-and-subscription",{"title":82,"path":83,"stem":84},"Plans and Limits","/account/plans-and-limits","3.account/4.plans-and-limits",{"title":86,"icon":87,"path":88,"stem":89,"children":90,"page":21},"Teams","i-lucide-users","/teams","4.teams",[91,95,99,103,107,111],{"title":92,"path":93,"stem":94},"Teams and Contexts","/teams/teams-and-contexts","4.teams/1.teams-and-contexts",{"title":96,"path":97,"stem":98},"Members, Roles and Seats","/teams/members-roles-and-seats","4.teams/2.members-roles-and-seats",{"title":100,"path":101,"stem":102},"Joining a Team","/teams/joining-a-team","4.teams/3.joining-a-team",{"title":104,"path":105,"stem":106},"Custom Branding","/teams/custom-branding","4.teams/4.custom-branding",{"title":108,"path":109,"stem":110},"Policy and Audit","/teams/policy-and-audit","4.teams/5.policy-and-audit",{"title":112,"path":113,"stem":114},"Zero-Knowledge for Teams","/teams/zero-knowledge-for-teams","4.teams/6.zero-knowledge-for-teams",{"title":116,"path":117,"stem":118,"children":119,"icon":130},"Help","/help","5.help/0.index",[120,122,126],{"title":121,"path":117,"stem":118},"Help and Support",{"title":123,"path":124,"stem":125},"Link Not Working","/help/link-not-working","5.help/1.link-not-working",{"title":127,"path":128,"stem":129},"Error Messages","/help/error-messages","5.help/2.error-messages","i-lucide-life-buoy",{"title":132,"path":133,"stem":134,"children":135},"API Reference","/api","6.api/0.index",[136,137,141,167,189,193,211,215],{"title":132,"path":133,"stem":134},{"title":138,"path":139,"stem":140},"Authentication","/api/authentication","6.api/1.authentication",{"title":142,"path":143,"stem":144,"children":145},"Shares","/api/shares","6.api/2.shares/0.index",[146,147,151,155,159,163],{"title":142,"path":143,"stem":144},{"title":148,"path":149,"stem":150},"Create a share","/api/shares/create","6.api/2.shares/1.create",{"title":152,"path":153,"stem":154},"List shares","/api/shares/list","6.api/2.shares/2.list",{"title":156,"path":157,"stem":158},"Retrieve a share","/api/shares/retrieve","6.api/2.shares/3.retrieve",{"title":160,"path":161,"stem":162},"Expire a share","/api/shares/delete","6.api/2.shares/4.delete",{"title":164,"path":165,"stem":166},"Client-side encryption","/api/shares/client-side-encryption","6.api/2.shares/5.client-side-encryption",{"title":168,"path":169,"stem":170,"children":171},"Secure requests","/api/requests","6.api/3.requests/0.index",[172,173,177,181,185],{"title":168,"path":169,"stem":170},{"title":174,"path":175,"stem":176},"Create a request","/api/requests/create","6.api/3.requests/1.create",{"title":178,"path":179,"stem":180},"List and retrieve requests","/api/requests/list","6.api/3.requests/2.list",{"title":182,"path":183,"stem":184},"Read submissions","/api/requests/submissions","6.api/3.requests/3.submissions",{"title":186,"path":187,"stem":188},"Expire or delete a request","/api/requests/delete","6.api/3.requests/4.delete",{"title":190,"path":191,"stem":192},"Stats","/api/stats","6.api/4.stats",{"title":194,"path":195,"stem":196,"children":197},"Webhooks","/api/webhooks","6.api/5.webhooks/0.index",[198,199,203,207],{"title":194,"path":195,"stem":196},{"title":200,"path":201,"stem":202},"Event reference","/api/webhooks/events","6.api/5.webhooks/1.events",{"title":204,"path":205,"stem":206},"Verifying signatures","/api/webhooks/verifying-signatures","6.api/5.webhooks/2.verifying-signatures",{"title":208,"path":209,"stem":210},"Delivery and retries","/api/webhooks/delivery-and-retries","6.api/5.webhooks/3.delivery-and-retries",{"title":212,"path":213,"stem":214},"MCP server","/api/mcp","6.api/6.mcp",{"title":216,"path":217,"stem":218},"Errors and Rate Limits","/api/errors-and-limits","6.api/7.errors-and-limits",{"title":220,"path":221,"stem":222,"children":223},"SDKs","/sdks","7.sdks/0.index",[224,225,229,233,237,241],{"title":220,"path":221,"stem":222},{"title":226,"path":227,"stem":228},"Node SDK","/sdks/node","7.sdks/1.node",{"title":230,"path":231,"stem":232},"Python SDK","/sdks/python","7.sdks/2.python",{"title":234,"path":235,"stem":236},"Go SDK","/sdks/go","7.sdks/3.go",{"title":238,"path":239,"stem":240},"Rust SDK","/sdks/rust","7.sdks/4.rust",{"title":242,"path":243,"stem":244},"Conformance vectors","/sdks/conformance-vectors","7.sdks/5.conformance-vectors",{"title":246,"path":247,"stem":248,"children":249,"page":21},"Security","/security","8.security",[250,254],{"title":251,"path":252,"stem":253},"Encryption","/security/encryption","8.security/1.encryption",{"title":255,"path":256,"stem":257},"Compliance","/security/compliance","8.security/2.compliance",{"id":259,"title":204,"api":260,"body":261,"description":2370,"extension":2371,"links":260,"meta":2372,"navigation":781,"path":205,"seo":2373,"stem":206,"__hash__":2374},"docs/6.api/5.webhooks/2.verifying-signatures.md",null,{"type":262,"value":263,"toc":2356},"minimark",[264,268,271,276,288,298,343,353,359,376,387,391,394,400,422,431,435,446,464,470,474,484,492,498,505,509,517,574,616,665,733,736,740,743,748,1440,1443,1695,1709,1711,2126,2137,2152,2156,2159,2171,2175,2185,2235,2238,2247,2250,2254,2334,2337,2352],[265,266,267],"p",{},"Anyone who learns your webhook URL can send JSON to it. The signature is how you tell a real CredenShare delivery from anything else, so verify it on every request before you act on the body.",[265,269,270],{},"Every delivery is signed with HMAC-SHA256 using a secret that belongs to that one endpoint. You are shown the secret once, when you create the endpoint, and once more each time you rotate it. It is never returned by any listing endpoint or tool, so if you lose it, rotate the endpoint to get a new one.",[272,273,275],"h2",{"id":274},"the-signature-header","The signature header",[265,277,278,279,283,284,287],{},"Each delivery carries a ",[280,281,282],"code",{},"X-CredenShare-Signature"," header. Its value is a comma-separated list of ",[280,285,286],{},"key=value"," pairs:",[289,290,296],"pre",{"className":291,"code":293,"language":294,"meta":295},[292],"language-text","X-CredenShare-Signature: t=\u003Cunix-seconds>,v1=\u003Chex>\n","text","",[280,297,293],{"__ignoreMap":295},[299,300,301,314],"table",{},[302,303,304],"thead",{},[305,306,307,311],"tr",{},[308,309,310],"th",{},"Key",[308,312,313],{},"Meaning",[315,316,317,328],"tbody",{},[305,318,319,325],{},[320,321,322],"td",{},[280,323,324],{},"t",[320,326,327],{},"The moment this attempt was signed, as decimal Unix seconds. Present exactly once.",[305,329,330,335],{},[320,331,332],{},[280,333,334],{},"v1",[320,336,337,338,342],{},"A signature, as lowercase hexadecimal (64 characters). Present ",[339,340,341],"strong",{},"one or more"," times.",[265,344,345,346,349,350,352],{},"During a secret rotation the header carries ",[339,347,348],{},"two"," ",[280,351,334],{}," values — the new secret's signature first, then the previous secret's:",[289,354,357],{"className":355,"code":356,"language":294,"meta":295},[292],"X-CredenShare-Signature: t=\u003Cunix-seconds>,v1=\u003Chex-new>,v1=\u003Chex-previous>\n",[280,358,356],{"__ignoreMap":295},[360,361,364],"callout",{"color":362,"icon":363},"warning","i-lucide-alert-triangle",[265,365,366,367,349,370,372,373,375],{},"Collect ",[339,368,369],{},"every",[280,371,334],{}," value and accept the delivery if any one of them matches. A verifier that reads only the first ",[280,374,334],{},", or only the last one, works most of the time and then fails for a whole day after you rotate a secret.",[265,377,378,379,382,383,386],{},"Parse the header by splitting on ",[280,380,381],{},","," and then on the first ",[280,384,385],{},"="," of each part, and look values up by name — do not rely on the order or the number of pairs. CredenShare sends no spaces around the separators, but trimming whitespace costs nothing.",[272,388,390],{"id":389},"what-is-signed","What is signed",[265,392,393],{},"The signed material is the timestamp, then a single ASCII period, then the raw bytes of the request body:",[289,395,398],{"className":396,"code":397,"language":294,"meta":295},[292],"signed    = \"\u003Ct>\" + \".\" + \u003Craw request body>\nsignature = hex( HMAC-SHA256(secret, signed) )\n",[280,399,397],{"__ignoreMap":295},[265,401,402,403,406,407,410,411,406,414,417,418,421],{},"The period matters. Without a separator the two values would run together, and different pairs would produce identical signed bytes: a timestamp of ",[280,404,405],{},"1"," with a body of ",[280,408,409],{},"23",", and a timestamp of ",[280,412,413],{},"12",[280,415,416],{},"3",", would both sign the string ",[280,419,420],{},"123",". A signature captured for one could then be presented as a valid signature for the other, which is exactly the substitution a signature exists to prevent. Use a period — not a colon, and not nothing.",[360,423,424],{"color":362,"icon":363},[265,425,426,427,430],{},"Hash the ",[339,428,429],{},"raw bytes as received",". Do not parse the JSON and re-serialize it before hashing. The byte sequence CredenShare signs is the body exactly as it is sent; its key order and whitespace are not guaranteed to survive a decode-and-re-encode round trip in your language, and any difference at all produces a completely different HMAC. In practice this means capturing the body before your web framework's JSON parser reaches it.",[272,432,434],{"id":433},"the-key-is-the-whole-secret","The key is the whole secret",[265,436,437,438,445],{},"The HMAC key is the ",[339,439,440,441,444],{},"full secret string, including its ",[280,442,443],{},"whsec_"," prefix",", taken as bytes.",[360,447,448],{"color":362,"icon":363},[265,449,450,451,454,455,457,458,460,461,463],{},"This is the detail that most often goes wrong. The key is the entire issued string, treated as opaque text. Do ",[339,452,453],{},"not"," strip the ",[280,456,443],{}," prefix, do ",[339,459,453],{}," base64-decode the part after the prefix, and do ",[339,462,453],{}," hash the secret before using it as the key. Each of those produces a valid-looking HMAC that will never match.",[265,465,466,467,469],{},"A secret looks like ",[280,468,443],{}," followed by URL-safe base64 characters. Treat it as an opaque string, store it wherever you keep your other credentials, and pass it to your HMAC function unmodified.",[272,471,473],{"id":472},"reject-stale-deliveries","Reject stale deliveries",[265,475,476,477,480,481,483],{},"The replay window is ",[339,478,479],{},"±5 minutes (300 seconds)",", measured symmetrically. Reject any delivery whose ",[280,482,324],{}," differs from your own clock by more than 300 seconds in either direction.",[265,485,486,487,491],{},"Check the window ",[488,489,490],"em",{},"before"," you compute any HMAC. It is the cheap check, and it is the part that stops a delivery captured off the wire from being re-posted to your endpoint an hour later.",[265,493,494,495,497],{},"The timestamp is generated at the moment of each attempt, not when the event happened. A retry of the same delivery therefore arrives with a fresh ",[280,496,324],{}," and a fresh signature over an unchanged body — so an old timestamp is never a legitimate retry arriving late, and you can keep the window tight without breaking retries.",[360,499,502],{"color":500,"icon":501},"info","i-lucide-info",[265,503,504],{},"Keep your receiver's clock synchronised. A host whose clock has drifted more than five minutes rejects every delivery, and the symptom is indistinguishable from a wrong secret.",[272,506,508],{"id":507},"use-an-sdk-if-you-can","Use an SDK if you can",[265,510,511,512,516],{},"All four ",[513,514,515],"a",{"href":221},"official SDKs"," ship a verifier. Each takes the raw body, the header value, and one or more secrets, and each reports failure by raising rather than by returning a falsy value you could forget to check:",[289,518,523],{"className":519,"code":520,"filename":521,"language":522,"meta":295,"style":295},"language-ts shiki shiki-themes github-light github-dark github-dark","import { webhooks } from '@credenshare/sdk'\nawait webhooks.verify(rawBody, header, [NEW_SECRET, OLD_SECRET])\n","Node","ts",[280,524,525,545],{"__ignoreMap":295},[526,527,530,534,538,541],"span",{"class":528,"line":529},"line",1,[526,531,533],{"class":532},"so5gQ","import",[526,535,537],{"class":536},"slsVL"," { webhooks } ",[526,539,540],{"class":532},"from",[526,542,544],{"class":543},"sfrk1"," '@credenshare/sdk'\n",[526,546,548,551,554,558,561,565,568,571],{"class":528,"line":547},2,[526,549,550],{"class":532},"await",[526,552,553],{"class":536}," webhooks.",[526,555,557],{"class":556},"shcOC","verify",[526,559,560],{"class":536},"(rawBody, header, [",[526,562,564],{"class":563},"suiK_","NEW_SECRET",[526,566,567],{"class":536},", ",[526,569,570],{"class":563},"OLD_SECRET",[526,572,573],{"class":536},"])\n",[289,575,580],{"className":576,"code":577,"filename":578,"language":579,"meta":295,"style":295},"language-python shiki shiki-themes github-light github-dark github-dark","from credenshare import webhooks\nwebhooks.verify(raw_body, header, secrets=[NEW_SECRET, OLD_SECRET])\n","Python","python",[280,581,582,594],{"__ignoreMap":295},[526,583,584,586,589,591],{"class":528,"line":529},[526,585,540],{"class":532},[526,587,588],{"class":536}," credenshare ",[526,590,533],{"class":532},[526,592,593],{"class":536}," webhooks\n",[526,595,596,599,603,605,608,610,612,614],{"class":528,"line":547},[526,597,598],{"class":536},"webhooks.verify(raw_body, header, ",[526,600,602],{"class":601},"sQHwn","secrets",[526,604,385],{"class":532},[526,606,607],{"class":536},"[",[526,609,564],{"class":563},[526,611,567],{"class":536},[526,613,570],{"class":563},[526,615,573],{"class":536},[289,617,622],{"className":618,"code":619,"filename":620,"language":621,"meta":295,"style":295},"language-go shiki shiki-themes github-light github-dark github-dark","import \"github.com/CredenShare/credenshare-sdk-go/webhooks\"\nerr := webhooks.Verify(rawBody, header, []string{newSecret, oldSecret}, nil)\n","Go","go",[280,623,624,637],{"__ignoreMap":295},[526,625,626,628,631,634],{"class":528,"line":529},[526,627,533],{"class":532},[526,629,630],{"class":543}," \"",[526,632,633],{"class":556},"github.com/CredenShare/credenshare-sdk-go/webhooks",[526,635,636],{"class":543},"\"\n",[526,638,639,642,645,647,650,653,656,659,662],{"class":528,"line":547},[526,640,641],{"class":536},"err ",[526,643,644],{"class":532},":=",[526,646,553],{"class":536},[526,648,649],{"class":556},"Verify",[526,651,652],{"class":536},"(rawBody, header, []",[526,654,655],{"class":532},"string",[526,657,658],{"class":536},"{newSecret, oldSecret}, ",[526,660,661],{"class":563},"nil",[526,663,664],{"class":536},")\n",[289,666,671],{"className":667,"code":668,"filename":669,"language":670,"meta":295,"style":295},"language-rust shiki shiki-themes github-light github-dark github-dark","use credenshare::webhooks;\nwebhooks::verify(raw_body, header, &[&new_secret, &old_secret], &Default::default())?;\n","Rust","rust",[280,672,673,687],{"__ignoreMap":295},[526,674,675,678,681,684],{"class":528,"line":529},[526,676,677],{"class":532},"use",[526,679,680],{"class":556}," credenshare",[526,682,683],{"class":532},"::",[526,685,686],{"class":536},"webhooks;\n",[526,688,689,692,694,696,699,702,704,706,709,711,714,716,719,721,724,727,730],{"class":528,"line":547},[526,690,691],{"class":556},"webhooks",[526,693,683],{"class":532},[526,695,557],{"class":556},[526,697,698],{"class":536},"(raw_body, header, ",[526,700,701],{"class":532},"&",[526,703,607],{"class":536},[526,705,701],{"class":532},[526,707,708],{"class":536},"new_secret, ",[526,710,701],{"class":532},[526,712,713],{"class":536},"old_secret], ",[526,715,701],{"class":532},[526,717,718],{"class":556},"Default",[526,720,683],{"class":532},[526,722,723],{"class":556},"default",[526,725,726],{"class":536},"())",[526,728,729],{"class":532},"?",[526,731,732],{"class":536},";\n",[265,734,735],{},"One caveat that applies to all four: the blank-secret check trims, but the HMAC is keyed with the untrimmed string. A secret read from a file with a trailing newline verifies against nothing. Trim it yourself.",[272,737,739],{"id":738},"a-correct-verifier","A correct verifier",[265,741,742],{},"The reference implementations below are for anyone not using an SDK.",[744,745,747],"h3",{"id":746},"nodejs","Node.js",[289,749,753],{"className":750,"code":751,"language":752,"meta":295,"style":295},"language-javascript shiki shiki-themes github-light github-dark github-dark","const crypto = require('node:crypto')\n\nconst REPLAY_WINDOW_SECONDS = 300\n\nfunction verifyCredenShareSignature(secret, rawBody, headerValue) {\n  if (!headerValue || !Buffer.isBuffer(rawBody)) return false\n\n  // Collect the timestamp and EVERY v1 signature.\n  let timestamp = null\n  const signatures = []\n  for (const part of headerValue.split(',')) {\n    const eq = part.indexOf('=')\n    if (eq === -1) continue\n    const key = part.slice(0, eq).trim()\n    const value = part.slice(eq + 1).trim()\n    if (key === 't') timestamp = value\n    else if (key === 'v1') signatures.push(value)\n  }\n  if (timestamp === null || signatures.length === 0) return false\n\n  // Enforce the replay window before doing any crypto.\n  const sentAt = Number.parseInt(timestamp, 10)\n  if (!Number.isInteger(sentAt)) return false\n  const now = Math.floor(Date.now() / 1000)\n  if (Math.abs(now - sentAt) > REPLAY_WINDOW_SECONDS) return false\n\n  // Sign \"\u003Ct>.\u003Craw body>\" with the full secret, prefix included.\n  const signed = Buffer.concat([Buffer.from(timestamp + '.', 'utf8'), rawBody])\n  const expected = crypto.createHmac('sha256', secret).update(signed).digest()\n\n  // Constant-time compare against each candidate; accept if any matches.\n  for (const candidate of signatures) {\n    const given = Buffer.from(candidate, 'hex')\n    if (given.length !== expected.length) continue\n    if (crypto.timingSafeEqual(given, expected)) return true\n  }\n  return false\n}\n","javascript",[280,754,755,777,783,796,801,828,864,869,876,890,904,934,958,981,1010,1040,1061,1086,1092,1126,1131,1137,1161,1183,1216,1247,1252,1258,1295,1330,1335,1341,1358,1380,1402,1421,1426,1434],{"__ignoreMap":295},[526,756,757,760,763,766,769,772,775],{"class":528,"line":529},[526,758,759],{"class":532},"const",[526,761,762],{"class":563}," crypto",[526,764,765],{"class":532}," =",[526,767,768],{"class":556}," require",[526,770,771],{"class":536},"(",[526,773,774],{"class":543},"'node:crypto'",[526,776,664],{"class":536},[526,778,779],{"class":528,"line":547},[526,780,782],{"emptyLinePlaceholder":781},true,"\n",[526,784,786,788,791,793],{"class":528,"line":785},3,[526,787,759],{"class":532},[526,789,790],{"class":563}," REPLAY_WINDOW_SECONDS",[526,792,765],{"class":532},[526,794,795],{"class":563}," 300\n",[526,797,799],{"class":528,"line":798},4,[526,800,782],{"emptyLinePlaceholder":781},[526,802,804,807,810,812,815,817,820,822,825],{"class":528,"line":803},5,[526,805,806],{"class":532},"function",[526,808,809],{"class":556}," verifyCredenShareSignature",[526,811,771],{"class":536},[526,813,814],{"class":601},"secret",[526,816,567],{"class":536},[526,818,819],{"class":601},"rawBody",[526,821,567],{"class":536},[526,823,824],{"class":601},"headerValue",[526,826,827],{"class":536},") {\n",[526,829,831,834,837,840,843,846,849,852,855,858,861],{"class":528,"line":830},6,[526,832,833],{"class":532},"  if",[526,835,836],{"class":536}," (",[526,838,839],{"class":532},"!",[526,841,842],{"class":536},"headerValue ",[526,844,845],{"class":532},"||",[526,847,848],{"class":532}," !",[526,850,851],{"class":536},"Buffer.",[526,853,854],{"class":556},"isBuffer",[526,856,857],{"class":536},"(rawBody)) ",[526,859,860],{"class":532},"return",[526,862,863],{"class":563}," false\n",[526,865,867],{"class":528,"line":866},7,[526,868,782],{"emptyLinePlaceholder":781},[526,870,872],{"class":528,"line":871},8,[526,873,875],{"class":874},"sCsY4","  // Collect the timestamp and EVERY v1 signature.\n",[526,877,879,882,885,887],{"class":528,"line":878},9,[526,880,881],{"class":532},"  let",[526,883,884],{"class":536}," timestamp ",[526,886,385],{"class":532},[526,888,889],{"class":563}," null\n",[526,891,893,896,899,901],{"class":528,"line":892},10,[526,894,895],{"class":532},"  const",[526,897,898],{"class":563}," signatures",[526,900,765],{"class":532},[526,902,903],{"class":536}," []\n",[526,905,907,910,912,914,917,920,923,926,928,931],{"class":528,"line":906},11,[526,908,909],{"class":532},"  for",[526,911,836],{"class":536},[526,913,759],{"class":532},[526,915,916],{"class":563}," part",[526,918,919],{"class":532}," of",[526,921,922],{"class":536}," headerValue.",[526,924,925],{"class":556},"split",[526,927,771],{"class":536},[526,929,930],{"class":543},"','",[526,932,933],{"class":536},")) {\n",[526,935,937,940,943,945,948,951,953,956],{"class":528,"line":936},12,[526,938,939],{"class":532},"    const",[526,941,942],{"class":563}," eq",[526,944,765],{"class":532},[526,946,947],{"class":536}," part.",[526,949,950],{"class":556},"indexOf",[526,952,771],{"class":536},[526,954,955],{"class":543},"'='",[526,957,664],{"class":536},[526,959,961,964,967,970,973,975,978],{"class":528,"line":960},13,[526,962,963],{"class":532},"    if",[526,965,966],{"class":536}," (eq ",[526,968,969],{"class":532},"===",[526,971,972],{"class":532}," -",[526,974,405],{"class":563},[526,976,977],{"class":536},") ",[526,979,980],{"class":532},"continue\n",[526,982,984,986,989,991,993,996,998,1001,1004,1007],{"class":528,"line":983},14,[526,985,939],{"class":532},[526,987,988],{"class":563}," key",[526,990,765],{"class":532},[526,992,947],{"class":536},[526,994,995],{"class":556},"slice",[526,997,771],{"class":536},[526,999,1000],{"class":563},"0",[526,1002,1003],{"class":536},", eq).",[526,1005,1006],{"class":556},"trim",[526,1008,1009],{"class":536},"()\n",[526,1011,1013,1015,1018,1020,1022,1024,1027,1030,1033,1036,1038],{"class":528,"line":1012},15,[526,1014,939],{"class":532},[526,1016,1017],{"class":563}," value",[526,1019,765],{"class":532},[526,1021,947],{"class":536},[526,1023,995],{"class":556},[526,1025,1026],{"class":536},"(eq ",[526,1028,1029],{"class":532},"+",[526,1031,1032],{"class":563}," 1",[526,1034,1035],{"class":536},").",[526,1037,1006],{"class":556},[526,1039,1009],{"class":536},[526,1041,1043,1045,1048,1050,1053,1056,1058],{"class":528,"line":1042},16,[526,1044,963],{"class":532},[526,1046,1047],{"class":536}," (key ",[526,1049,969],{"class":532},[526,1051,1052],{"class":543}," 't'",[526,1054,1055],{"class":536},") timestamp ",[526,1057,385],{"class":532},[526,1059,1060],{"class":536}," value\n",[526,1062,1064,1067,1070,1072,1074,1077,1080,1083],{"class":528,"line":1063},17,[526,1065,1066],{"class":532},"    else",[526,1068,1069],{"class":532}," if",[526,1071,1047],{"class":536},[526,1073,969],{"class":532},[526,1075,1076],{"class":543}," 'v1'",[526,1078,1079],{"class":536},") signatures.",[526,1081,1082],{"class":556},"push",[526,1084,1085],{"class":536},"(value)\n",[526,1087,1089],{"class":528,"line":1088},18,[526,1090,1091],{"class":536},"  }\n",[526,1093,1095,1097,1100,1102,1105,1108,1111,1114,1117,1120,1122,1124],{"class":528,"line":1094},19,[526,1096,833],{"class":532},[526,1098,1099],{"class":536}," (timestamp ",[526,1101,969],{"class":532},[526,1103,1104],{"class":563}," null",[526,1106,1107],{"class":532}," ||",[526,1109,1110],{"class":536}," signatures.",[526,1112,1113],{"class":563},"length",[526,1115,1116],{"class":532}," ===",[526,1118,1119],{"class":563}," 0",[526,1121,977],{"class":536},[526,1123,860],{"class":532},[526,1125,863],{"class":563},[526,1127,1129],{"class":528,"line":1128},20,[526,1130,782],{"emptyLinePlaceholder":781},[526,1132,1134],{"class":528,"line":1133},21,[526,1135,1136],{"class":874},"  // Enforce the replay window before doing any crypto.\n",[526,1138,1140,1142,1145,1147,1150,1153,1156,1159],{"class":528,"line":1139},22,[526,1141,895],{"class":532},[526,1143,1144],{"class":563}," sentAt",[526,1146,765],{"class":532},[526,1148,1149],{"class":536}," Number.",[526,1151,1152],{"class":556},"parseInt",[526,1154,1155],{"class":536},"(timestamp, ",[526,1157,1158],{"class":563},"10",[526,1160,664],{"class":536},[526,1162,1164,1166,1168,1170,1173,1176,1179,1181],{"class":528,"line":1163},23,[526,1165,833],{"class":532},[526,1167,836],{"class":536},[526,1169,839],{"class":532},[526,1171,1172],{"class":536},"Number.",[526,1174,1175],{"class":556},"isInteger",[526,1177,1178],{"class":536},"(sentAt)) ",[526,1180,860],{"class":532},[526,1182,863],{"class":563},[526,1184,1186,1188,1191,1193,1196,1199,1202,1205,1208,1211,1214],{"class":528,"line":1185},24,[526,1187,895],{"class":532},[526,1189,1190],{"class":563}," now",[526,1192,765],{"class":532},[526,1194,1195],{"class":536}," Math.",[526,1197,1198],{"class":556},"floor",[526,1200,1201],{"class":536},"(Date.",[526,1203,1204],{"class":556},"now",[526,1206,1207],{"class":536},"() ",[526,1209,1210],{"class":532},"/",[526,1212,1213],{"class":563}," 1000",[526,1215,664],{"class":536},[526,1217,1219,1221,1224,1227,1230,1233,1236,1239,1241,1243,1245],{"class":528,"line":1218},25,[526,1220,833],{"class":532},[526,1222,1223],{"class":536}," (Math.",[526,1225,1226],{"class":556},"abs",[526,1228,1229],{"class":536},"(now ",[526,1231,1232],{"class":532},"-",[526,1234,1235],{"class":536}," sentAt) ",[526,1237,1238],{"class":532},">",[526,1240,790],{"class":563},[526,1242,977],{"class":536},[526,1244,860],{"class":532},[526,1246,863],{"class":563},[526,1248,1250],{"class":528,"line":1249},26,[526,1251,782],{"emptyLinePlaceholder":781},[526,1253,1255],{"class":528,"line":1254},27,[526,1256,1257],{"class":874},"  // Sign \"\u003Ct>.\u003Craw body>\" with the full secret, prefix included.\n",[526,1259,1261,1263,1266,1268,1271,1274,1277,1279,1282,1284,1287,1289,1292],{"class":528,"line":1260},28,[526,1262,895],{"class":532},[526,1264,1265],{"class":563}," signed",[526,1267,765],{"class":532},[526,1269,1270],{"class":536}," Buffer.",[526,1272,1273],{"class":556},"concat",[526,1275,1276],{"class":536},"([Buffer.",[526,1278,540],{"class":556},[526,1280,1281],{"class":536},"(timestamp ",[526,1283,1029],{"class":532},[526,1285,1286],{"class":543}," '.'",[526,1288,567],{"class":536},[526,1290,1291],{"class":543},"'utf8'",[526,1293,1294],{"class":536},"), rawBody])\n",[526,1296,1298,1300,1303,1305,1308,1311,1313,1316,1319,1322,1325,1328],{"class":528,"line":1297},29,[526,1299,895],{"class":532},[526,1301,1302],{"class":563}," expected",[526,1304,765],{"class":532},[526,1306,1307],{"class":536}," crypto.",[526,1309,1310],{"class":556},"createHmac",[526,1312,771],{"class":536},[526,1314,1315],{"class":543},"'sha256'",[526,1317,1318],{"class":536},", secret).",[526,1320,1321],{"class":556},"update",[526,1323,1324],{"class":536},"(signed).",[526,1326,1327],{"class":556},"digest",[526,1329,1009],{"class":536},[526,1331,1333],{"class":528,"line":1332},30,[526,1334,782],{"emptyLinePlaceholder":781},[526,1336,1338],{"class":528,"line":1337},31,[526,1339,1340],{"class":874},"  // Constant-time compare against each candidate; accept if any matches.\n",[526,1342,1344,1346,1348,1350,1353,1355],{"class":528,"line":1343},32,[526,1345,909],{"class":532},[526,1347,836],{"class":536},[526,1349,759],{"class":532},[526,1351,1352],{"class":563}," candidate",[526,1354,919],{"class":532},[526,1356,1357],{"class":536}," signatures) {\n",[526,1359,1361,1363,1366,1368,1370,1372,1375,1378],{"class":528,"line":1360},33,[526,1362,939],{"class":532},[526,1364,1365],{"class":563}," given",[526,1367,765],{"class":532},[526,1369,1270],{"class":536},[526,1371,540],{"class":556},[526,1373,1374],{"class":536},"(candidate, ",[526,1376,1377],{"class":543},"'hex'",[526,1379,664],{"class":536},[526,1381,1383,1385,1388,1390,1393,1396,1398,1400],{"class":528,"line":1382},34,[526,1384,963],{"class":532},[526,1386,1387],{"class":536}," (given.",[526,1389,1113],{"class":563},[526,1391,1392],{"class":532}," !==",[526,1394,1395],{"class":536}," expected.",[526,1397,1113],{"class":563},[526,1399,977],{"class":536},[526,1401,980],{"class":532},[526,1403,1405,1407,1410,1413,1416,1418],{"class":528,"line":1404},35,[526,1406,963],{"class":532},[526,1408,1409],{"class":536}," (crypto.",[526,1411,1412],{"class":556},"timingSafeEqual",[526,1414,1415],{"class":536},"(given, expected)) ",[526,1417,860],{"class":532},[526,1419,1420],{"class":563}," true\n",[526,1422,1424],{"class":528,"line":1423},36,[526,1425,1091],{"class":536},[526,1427,1429,1432],{"class":528,"line":1428},37,[526,1430,1431],{"class":532},"  return",[526,1433,863],{"class":563},[526,1435,1437],{"class":528,"line":1436},38,[526,1438,1439],{"class":536},"}\n",[265,1441,1442],{},"Wiring it into Express, taking care to keep the raw body:",[289,1444,1446],{"className":750,"code":1445,"language":752,"meta":295,"style":295},"const express = require('express')\nconst app = express()\n\napp.post(\n  '/credenshare-webhook',\n  express.raw({ type: 'application/json' }),\n  (req, res) => {\n    const ok = verifyCredenShareSignature(\n      process.env.CREDENSHARE_WEBHOOK_SECRET,\n      req.body,\n      req.get('X-CredenShare-Signature')\n    )\n    if (!ok) return res.status(400).send('invalid signature')\n\n    const event = JSON.parse(req.body.toString('utf8'))\n    // Queue the work, then answer quickly.\n    res.status(200).send('ok')\n  }\n)\n",[280,1447,1448,1466,1479,1483,1494,1502,1519,1540,1553,1563,1568,1583,1588,1624,1628,1659,1664,1687,1691],{"__ignoreMap":295},[526,1449,1450,1452,1455,1457,1459,1461,1464],{"class":528,"line":529},[526,1451,759],{"class":532},[526,1453,1454],{"class":563}," express",[526,1456,765],{"class":532},[526,1458,768],{"class":556},[526,1460,771],{"class":536},[526,1462,1463],{"class":543},"'express'",[526,1465,664],{"class":536},[526,1467,1468,1470,1473,1475,1477],{"class":528,"line":547},[526,1469,759],{"class":532},[526,1471,1472],{"class":563}," app",[526,1474,765],{"class":532},[526,1476,1454],{"class":556},[526,1478,1009],{"class":536},[526,1480,1481],{"class":528,"line":785},[526,1482,782],{"emptyLinePlaceholder":781},[526,1484,1485,1488,1491],{"class":528,"line":798},[526,1486,1487],{"class":536},"app.",[526,1489,1490],{"class":556},"post",[526,1492,1493],{"class":536},"(\n",[526,1495,1496,1499],{"class":528,"line":803},[526,1497,1498],{"class":543},"  '/credenshare-webhook'",[526,1500,1501],{"class":536},",\n",[526,1503,1504,1507,1510,1513,1516],{"class":528,"line":830},[526,1505,1506],{"class":536},"  express.",[526,1508,1509],{"class":556},"raw",[526,1511,1512],{"class":536},"({ type: ",[526,1514,1515],{"class":543},"'application/json'",[526,1517,1518],{"class":536}," }),\n",[526,1520,1521,1524,1527,1529,1532,1534,1537],{"class":528,"line":866},[526,1522,1523],{"class":536},"  (",[526,1525,1526],{"class":601},"req",[526,1528,567],{"class":536},[526,1530,1531],{"class":601},"res",[526,1533,977],{"class":536},[526,1535,1536],{"class":532},"=>",[526,1538,1539],{"class":536}," {\n",[526,1541,1542,1544,1547,1549,1551],{"class":528,"line":871},[526,1543,939],{"class":532},[526,1545,1546],{"class":563}," ok",[526,1548,765],{"class":532},[526,1550,809],{"class":556},[526,1552,1493],{"class":536},[526,1554,1555,1558,1561],{"class":528,"line":878},[526,1556,1557],{"class":536},"      process.env.",[526,1559,1560],{"class":563},"CREDENSHARE_WEBHOOK_SECRET",[526,1562,1501],{"class":536},[526,1564,1565],{"class":528,"line":892},[526,1566,1567],{"class":536},"      req.body,\n",[526,1569,1570,1573,1576,1578,1581],{"class":528,"line":906},[526,1571,1572],{"class":536},"      req.",[526,1574,1575],{"class":556},"get",[526,1577,771],{"class":536},[526,1579,1580],{"class":543},"'X-CredenShare-Signature'",[526,1582,664],{"class":536},[526,1584,1585],{"class":528,"line":936},[526,1586,1587],{"class":536},"    )\n",[526,1589,1590,1592,1594,1596,1599,1601,1604,1607,1609,1612,1614,1617,1619,1622],{"class":528,"line":960},[526,1591,963],{"class":532},[526,1593,836],{"class":536},[526,1595,839],{"class":532},[526,1597,1598],{"class":536},"ok) ",[526,1600,860],{"class":532},[526,1602,1603],{"class":536}," res.",[526,1605,1606],{"class":556},"status",[526,1608,771],{"class":536},[526,1610,1611],{"class":563},"400",[526,1613,1035],{"class":536},[526,1615,1616],{"class":556},"send",[526,1618,771],{"class":536},[526,1620,1621],{"class":543},"'invalid signature'",[526,1623,664],{"class":536},[526,1625,1626],{"class":528,"line":983},[526,1627,782],{"emptyLinePlaceholder":781},[526,1629,1630,1632,1635,1637,1640,1643,1646,1649,1652,1654,1656],{"class":528,"line":1012},[526,1631,939],{"class":532},[526,1633,1634],{"class":563}," event",[526,1636,765],{"class":532},[526,1638,1639],{"class":563}," JSON",[526,1641,1642],{"class":536},".",[526,1644,1645],{"class":556},"parse",[526,1647,1648],{"class":536},"(req.body.",[526,1650,1651],{"class":556},"toString",[526,1653,771],{"class":536},[526,1655,1291],{"class":543},[526,1657,1658],{"class":536},"))\n",[526,1660,1661],{"class":528,"line":1042},[526,1662,1663],{"class":874},"    // Queue the work, then answer quickly.\n",[526,1665,1666,1669,1671,1673,1676,1678,1680,1682,1685],{"class":528,"line":1063},[526,1667,1668],{"class":536},"    res.",[526,1670,1606],{"class":556},[526,1672,771],{"class":536},[526,1674,1675],{"class":563},"200",[526,1677,1035],{"class":536},[526,1679,1616],{"class":556},[526,1681,771],{"class":536},[526,1683,1684],{"class":543},"'ok'",[526,1686,664],{"class":536},[526,1688,1689],{"class":528,"line":1088},[526,1690,1091],{"class":536},[526,1692,1693],{"class":528,"line":1094},[526,1694,664],{"class":536},[265,1696,1697,1700,1701,1704,1705,1708],{},[280,1698,1699],{},"express.raw()"," hands you a ",[280,1702,1703],{},"Buffer"," holding the untouched body. Do not mount ",[280,1706,1707],{},"express.json()"," on this route: it replaces the body with a parsed object, and the bytes that were signed are gone.",[744,1710,578],{"id":579},[289,1712,1714],{"className":576,"code":1713,"language":579,"meta":295,"style":295},"import hashlib\nimport hmac\nimport time\n\nREPLAY_WINDOW_SECONDS = 300\n\n\ndef verify_credenshare_signature(secret: str, raw_body: bytes, header: str) -> bool:\n    if not header:\n        return False\n\n    # Collect the timestamp and EVERY v1 signature.\n    timestamp = None\n    signatures = []\n    for part in header.split(\",\"):\n        key, _, value = part.strip().partition(\"=\")\n        if key == \"t\":\n            timestamp = value\n        elif key == \"v1\":\n            signatures.append(value)\n    if timestamp is None or not signatures:\n        return False\n\n    # Enforce the replay window before doing any crypto.\n    try:\n        sent_at = int(timestamp)\n    except ValueError:\n        return False\n    if abs(int(time.time()) - sent_at) > REPLAY_WINDOW_SECONDS:\n        return False\n\n    # Sign \"\u003Ct>.\u003Craw body>\" with the full secret, prefix included.\n    signed = timestamp.encode(\"ascii\") + b\".\" + raw_body\n    expected = hmac.new(secret.encode(\"utf-8\"), signed, hashlib.sha256).hexdigest()\n\n    # Constant-time compare against each candidate; accept if any matches.\n    return any(\n        hmac.compare_digest(expected.encode(\"ascii\"), candidate.encode(\"utf-8\"))\n        for candidate in signatures\n    )\n",[280,1715,1716,1723,1730,1737,1741,1750,1754,1758,1792,1802,1810,1814,1819,1829,1838,1858,1873,1889,1898,1912,1917,1937,1943,1947,1952,1959,1972,1982,1988,2014,2020,2024,2029,2058,2074,2078,2083,2093,2107,2121],{"__ignoreMap":295},[526,1717,1718,1720],{"class":528,"line":529},[526,1719,533],{"class":532},[526,1721,1722],{"class":536}," hashlib\n",[526,1724,1725,1727],{"class":528,"line":547},[526,1726,533],{"class":532},[526,1728,1729],{"class":536}," hmac\n",[526,1731,1732,1734],{"class":528,"line":785},[526,1733,533],{"class":532},[526,1735,1736],{"class":536}," time\n",[526,1738,1739],{"class":528,"line":798},[526,1740,782],{"emptyLinePlaceholder":781},[526,1742,1743,1746,1748],{"class":528,"line":803},[526,1744,1745],{"class":563},"REPLAY_WINDOW_SECONDS",[526,1747,765],{"class":532},[526,1749,795],{"class":563},[526,1751,1752],{"class":528,"line":830},[526,1753,782],{"emptyLinePlaceholder":781},[526,1755,1756],{"class":528,"line":866},[526,1757,782],{"emptyLinePlaceholder":781},[526,1759,1760,1763,1766,1769,1772,1775,1778,1781,1783,1786,1789],{"class":528,"line":871},[526,1761,1762],{"class":532},"def",[526,1764,1765],{"class":556}," verify_credenshare_signature",[526,1767,1768],{"class":536},"(secret: ",[526,1770,1771],{"class":563},"str",[526,1773,1774],{"class":536},", raw_body: ",[526,1776,1777],{"class":563},"bytes",[526,1779,1780],{"class":536},", header: ",[526,1782,1771],{"class":563},[526,1784,1785],{"class":536},") -> ",[526,1787,1788],{"class":563},"bool",[526,1790,1791],{"class":536},":\n",[526,1793,1794,1796,1799],{"class":528,"line":878},[526,1795,963],{"class":532},[526,1797,1798],{"class":532}," not",[526,1800,1801],{"class":536}," header:\n",[526,1803,1804,1807],{"class":528,"line":892},[526,1805,1806],{"class":532},"        return",[526,1808,1809],{"class":563}," False\n",[526,1811,1812],{"class":528,"line":906},[526,1813,782],{"emptyLinePlaceholder":781},[526,1815,1816],{"class":528,"line":936},[526,1817,1818],{"class":874},"    # Collect the timestamp and EVERY v1 signature.\n",[526,1820,1821,1824,1826],{"class":528,"line":960},[526,1822,1823],{"class":536},"    timestamp ",[526,1825,385],{"class":532},[526,1827,1828],{"class":563}," None\n",[526,1830,1831,1834,1836],{"class":528,"line":983},[526,1832,1833],{"class":536},"    signatures ",[526,1835,385],{"class":532},[526,1837,903],{"class":536},[526,1839,1840,1843,1846,1849,1852,1855],{"class":528,"line":1012},[526,1841,1842],{"class":532},"    for",[526,1844,1845],{"class":536}," part ",[526,1847,1848],{"class":532},"in",[526,1850,1851],{"class":536}," header.split(",[526,1853,1854],{"class":543},"\",\"",[526,1856,1857],{"class":536},"):\n",[526,1859,1860,1863,1865,1868,1871],{"class":528,"line":1042},[526,1861,1862],{"class":536},"        key, _, value ",[526,1864,385],{"class":532},[526,1866,1867],{"class":536}," part.strip().partition(",[526,1869,1870],{"class":543},"\"=\"",[526,1872,664],{"class":536},[526,1874,1875,1878,1881,1884,1887],{"class":528,"line":1063},[526,1876,1877],{"class":532},"        if",[526,1879,1880],{"class":536}," key ",[526,1882,1883],{"class":532},"==",[526,1885,1886],{"class":543}," \"t\"",[526,1888,1791],{"class":536},[526,1890,1891,1894,1896],{"class":528,"line":1088},[526,1892,1893],{"class":536},"            timestamp ",[526,1895,385],{"class":532},[526,1897,1060],{"class":536},[526,1899,1900,1903,1905,1907,1910],{"class":528,"line":1094},[526,1901,1902],{"class":532},"        elif",[526,1904,1880],{"class":536},[526,1906,1883],{"class":532},[526,1908,1909],{"class":543}," \"v1\"",[526,1911,1791],{"class":536},[526,1913,1914],{"class":528,"line":1128},[526,1915,1916],{"class":536},"            signatures.append(value)\n",[526,1918,1919,1921,1923,1926,1929,1932,1934],{"class":528,"line":1133},[526,1920,963],{"class":532},[526,1922,884],{"class":536},[526,1924,1925],{"class":532},"is",[526,1927,1928],{"class":563}," None",[526,1930,1931],{"class":532}," or",[526,1933,1798],{"class":532},[526,1935,1936],{"class":536}," signatures:\n",[526,1938,1939,1941],{"class":528,"line":1139},[526,1940,1806],{"class":532},[526,1942,1809],{"class":563},[526,1944,1945],{"class":528,"line":1163},[526,1946,782],{"emptyLinePlaceholder":781},[526,1948,1949],{"class":528,"line":1185},[526,1950,1951],{"class":874},"    # Enforce the replay window before doing any crypto.\n",[526,1953,1954,1957],{"class":528,"line":1218},[526,1955,1956],{"class":532},"    try",[526,1958,1791],{"class":536},[526,1960,1961,1964,1966,1969],{"class":528,"line":1249},[526,1962,1963],{"class":536},"        sent_at ",[526,1965,385],{"class":532},[526,1967,1968],{"class":563}," int",[526,1970,1971],{"class":536},"(timestamp)\n",[526,1973,1974,1977,1980],{"class":528,"line":1254},[526,1975,1976],{"class":532},"    except",[526,1978,1979],{"class":563}," ValueError",[526,1981,1791],{"class":536},[526,1983,1984,1986],{"class":528,"line":1260},[526,1985,1806],{"class":532},[526,1987,1809],{"class":563},[526,1989,1990,1992,1995,1997,2000,2003,2005,2008,2010,2012],{"class":528,"line":1297},[526,1991,963],{"class":532},[526,1993,1994],{"class":563}," abs",[526,1996,771],{"class":536},[526,1998,1999],{"class":563},"int",[526,2001,2002],{"class":536},"(time.time()) ",[526,2004,1232],{"class":532},[526,2006,2007],{"class":536}," sent_at) ",[526,2009,1238],{"class":532},[526,2011,790],{"class":563},[526,2013,1791],{"class":536},[526,2015,2016,2018],{"class":528,"line":1332},[526,2017,1806],{"class":532},[526,2019,1809],{"class":563},[526,2021,2022],{"class":528,"line":1337},[526,2023,782],{"emptyLinePlaceholder":781},[526,2025,2026],{"class":528,"line":1343},[526,2027,2028],{"class":874},"    # Sign \"\u003Ct>.\u003Craw body>\" with the full secret, prefix included.\n",[526,2030,2031,2034,2036,2039,2042,2044,2046,2049,2052,2055],{"class":528,"line":1360},[526,2032,2033],{"class":536},"    signed ",[526,2035,385],{"class":532},[526,2037,2038],{"class":536}," timestamp.encode(",[526,2040,2041],{"class":543},"\"ascii\"",[526,2043,977],{"class":536},[526,2045,1029],{"class":532},[526,2047,2048],{"class":532}," b",[526,2050,2051],{"class":543},"\".\"",[526,2053,2054],{"class":532}," +",[526,2056,2057],{"class":536}," raw_body\n",[526,2059,2060,2063,2065,2068,2071],{"class":528,"line":1382},[526,2061,2062],{"class":536},"    expected ",[526,2064,385],{"class":532},[526,2066,2067],{"class":536}," hmac.new(secret.encode(",[526,2069,2070],{"class":543},"\"utf-8\"",[526,2072,2073],{"class":536},"), signed, hashlib.sha256).hexdigest()\n",[526,2075,2076],{"class":528,"line":1404},[526,2077,782],{"emptyLinePlaceholder":781},[526,2079,2080],{"class":528,"line":1423},[526,2081,2082],{"class":874},"    # Constant-time compare against each candidate; accept if any matches.\n",[526,2084,2085,2088,2091],{"class":528,"line":1428},[526,2086,2087],{"class":532},"    return",[526,2089,2090],{"class":563}," any",[526,2092,1493],{"class":536},[526,2094,2095,2098,2100,2103,2105],{"class":528,"line":1436},[526,2096,2097],{"class":536},"        hmac.compare_digest(expected.encode(",[526,2099,2041],{"class":543},[526,2101,2102],{"class":536},"), candidate.encode(",[526,2104,2070],{"class":543},[526,2106,1658],{"class":536},[526,2108,2110,2113,2116,2118],{"class":528,"line":2109},39,[526,2111,2112],{"class":532},"        for",[526,2114,2115],{"class":536}," candidate ",[526,2117,1848],{"class":532},[526,2119,2120],{"class":536}," signatures\n",[526,2122,2124],{"class":528,"line":2123},40,[526,2125,1587],{"class":536},[265,2127,2128,2129,2132,2133,2136],{},"In Flask, read the body with ",[280,2130,2131],{},"request.get_data()"," — never ",[280,2134,2135],{},"request.get_json()"," — so that you hash the bytes that were actually signed.",[360,2138,2139],{"color":500,"icon":501},[265,2140,2141,2142,2144,2145,567,2148,2151],{},"Compare in constant time. A plain ",[280,2143,1883],{}," on the hex strings leaks, through timing, how many leading characters a guess got right, which turns forging a signature into a character-by-character search. Use ",[280,2146,2147],{},"crypto.timingSafeEqual",[280,2149,2150],{},"hmac.compare_digest",", or your language's equivalent.",[272,2153,2155],{"id":2154},"what-to-return-when-verification-fails","What to return when verification fails",[265,2157,2158],{},"If verification fails, the cause is almost always configuration on your side: the wrong secret, a body that was parsed before it was hashed, or a skewed clock.",[265,2160,2161,2162,2164,2165,2168,2169,1642],{},"Your status code decides what happens to that delivery. A ",[280,2163,1611],{}," marks it permanently failed, and CredenShare will not attempt it again. If you would rather the delivery be retried while you fix the problem, answer ",[280,2166,2167],{},"500"," — it stays on the retry schedule. See ",[513,2170,208],{"href":209},[272,2172,2174],{"id":2173},"rotating-the-secret","Rotating the secret",[265,2176,2177,2178,2180,2181,2184],{},"Rotate an endpoint's secret from the ",[339,2179,194],{}," card in your account's Security settings, or with the hosted MCP server's ",[280,2182,2183],{},"rotate_webhook_secret"," tool. Either way the new secret is shown once, at that moment, and not again.",[299,2186,2187,2196],{},[302,2188,2189],{},[305,2190,2191,2193],{},[308,2192],{},[308,2194,2195],{},"Behaviour",[315,2197,2198,2206,2219,2227],{},[305,2199,2200,2203],{},[320,2201,2202],{},"Grace window",[320,2204,2205],{},"24 hours from the moment you rotate.",[305,2207,2208,2211],{},[320,2209,2210],{},"During the window",[320,2212,2213,2214,349,2216,2218],{},"Every delivery carries ",[339,2215,348],{},[280,2217,334],{}," values: the new secret's signature first, then the previous secret's.",[305,2220,2221,2224],{},[320,2222,2223],{},"After the window",[320,2225,2226],{},"Only the new secret signs. The previous one stops verifying.",[305,2228,2229,2232],{},[320,2230,2231],{},"Deliveries in flight",[320,2233,2234],{},"Anything not yet sent — queued, or failed and awaiting a retry — is signed with the new secret plus the old one for the rest of the window. Deliveries already sent are untouched, and nothing is re-signed retroactively.",[265,2236,2237],{},"Because both signatures are present for a full day, you can roll your configuration at your own pace without dropping a delivery: rotate, deploy the new secret whenever your release process allows, and the old one keeps verifying until you have.",[360,2239,2240],{"color":362,"icon":363},[265,2241,2242,2243,2246],{},"Only ",[339,2244,2245],{},"one"," previous secret is retained. Rotating twice inside the same 24-hour window discards the oldest immediately, so any receiver still using the original secret starts failing at once. Let the window close before you rotate again.",[265,2248,2249],{},"If an endpoint is ever shown as disabled with a reason saying the signing key changed, its secret can no longer produce verifiable signatures and rotation will not recover it. Create a new endpoint and configure your receiver with the new secret.",[272,2251,2253],{"id":2252},"common-mistakes","Common mistakes",[299,2255,2256,2266],{},[302,2257,2258],{},[305,2259,2260,2263],{},[308,2261,2262],{},"Mistake",[308,2264,2265],{},"Symptom",[315,2267,2268,2279,2286,2298,2306,2316,2324],{},[305,2269,2270,2276],{},[320,2271,2272,2273,2275],{},"Stripping or decoding the ",[280,2274,443],{}," prefix before using the secret as the key",[320,2277,2278],{},"Every signature fails, on every delivery",[305,2280,2281,2284],{},[320,2282,2283],{},"Hashing the secret and using the digest as the key",[320,2285,2278],{},[305,2287,2288,2295],{},[320,2289,2290,2291,2294],{},"HMAC over the body alone, without ",[280,2292,2293],{},"\u003Ct>."," in front",[320,2296,2297],{},"Every signature fails",[305,2299,2300,2303],{},[320,2301,2302],{},"Re-serializing the parsed JSON before hashing",[320,2304,2305],{},"Fails intermittently and unreproducibly, depending on the payload",[305,2307,2308,2313],{},[320,2309,2310,2311,1017],{},"Reading only one ",[280,2312,334],{},[320,2314,2315],{},"Works, then fails for 24 hours after every rotation",[305,2317,2318,2321],{},[320,2319,2320],{},"No replay-window check",[320,2322,2323],{},"Signatures verify, but a captured delivery can be re-posted indefinitely",[305,2325,2326,2331],{},[320,2327,2328,2329],{},"Comparing hex strings with ",[280,2330,1883],{},[320,2332,2333],{},"Verifies correctly, but leaks the timing information that makes a forgery practical",[2335,2336],"hr",{},[360,2338,2340],{"color":500,"icon":2339},"i-lucide-arrow-right",[265,2341,2342,2343,2345,2346,1642],{},"The ",[513,2344,515],{"href":221}," verify signatures in one line, and are the shortest path through this page. If you are evaluating rather than integrating, ",[513,2347,2351],{"href":2348,"rel":2349},"https://credenshare.io/pricing",[2350],"nofollow","start free",[2353,2354,2355],"style",{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}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 .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":295,"searchDepth":529,"depth":547,"links":2357},[2358,2359,2360,2361,2362,2363,2367,2368,2369],{"id":274,"depth":547,"text":275},{"id":389,"depth":547,"text":390},{"id":433,"depth":547,"text":434},{"id":472,"depth":547,"text":473},{"id":507,"depth":547,"text":508},{"id":738,"depth":547,"text":739,"children":2364},[2365,2366],{"id":746,"depth":785,"text":747},{"id":579,"depth":785,"text":578},{"id":2154,"depth":547,"text":2155},{"id":2173,"depth":547,"text":2174},{"id":2252,"depth":547,"text":2253},"How CredenShare signs every webhook delivery, and how to verify one correctly.","md",{},{"title":204,"description":2370},"oLfcHcsZhf-u52hs37RSzGwrjOteEacWGZXWJshcZxQ",[2376,2378],{"title":200,"path":201,"stem":202,"description":2377,"children":-1},"The nineteen webhook event codes, when each one fires, and the exact fields of a delivery payload.",{"title":208,"path":209,"stem":210,"description":2379,"children":-1},"The request CredenShare sends, what counts as success, the retry schedule, and how to recover a delivery that failed.",1788908853238]