[{"data":1,"prerenderedAt":617},["ShallowReactive",2],{"content-query-5JVsshPLE1":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"crumb":11,"term":11,"summary":12,"group":13,"order":14,"updated":15,"cta":16,"related":24,"faq":43,"body":56,"_type":610,"_id":611,"_source":612,"_file":613,"_stem":614,"_extension":615,"sitemap":616},"/glossary/webhook-secret","glossary",false,"","What is a webhook secret? Signing secrets explained","A webhook secret is a shared key the provider uses to sign each webhook and you use to verify it. Where to find it, how to store and rotate it, and common mistakes.","reference","Webhook secret","The shared key used to sign and verify webhooks.","Security",11,"2026-10-28",{"eyebrow":17,"title":18,"text":19,"to":20,"label":21,"secondaryTo":22,"secondaryLabel":23},"Free tool","Test your secret against a real signature","Paste a webhook body, its signature header and your secret into the webhook signature verifier to see whether they match. It runs in your browser, so the secret never leaves your machine.","/verify-webhook-signature/","Verify a signature","/standard-webhooks-signature-generator/","Generate a test signature",[25,28,31,34,37,40],{"title":26,"to":27},"Webhook signature","/glossary/webhook-signature/",{"title":29,"to":30},"HMAC","/glossary/hmac/",{"title":32,"to":33},"Standard Webhooks","/glossary/standard-webhooks/",{"title":35,"to":36},"401 vs 403","/http-errors/401-vs-403/",{"title":38,"to":39},"Webhook security","/blog/webhook-security/",{"title":41,"to":42},"HMAC calculator","/hmac-verification/",[44,47,50,53],{"q":45,"a":46},"What is a webhook secret?","A webhook secret, or signing secret, is a random key shared between the webhook provider and your endpoint. The provider uses it to compute an HMAC signature for every delivery, and your endpoint uses the same key to recompute the signature and confirm the request is genuine and unmodified.",{"q":48,"a":49},"Where do I find my webhook secret?","In the provider's webhook settings, usually shown when you create the endpoint. Stripe shows a signing secret starting with whsec_ on each endpoint's page. GitHub asks you to choose the secret yourself when you add the webhook. Standard Webhooks providers issue secrets prefixed with whsec_.",{"q":51,"a":52},"Is a webhook secret the same as an API key?","No. An API key authenticates your requests to the provider's API. A webhook secret verifies requests the provider sends to you. Keep them separate, and never use an API key as a webhook secret.",{"q":54,"a":55},"How do I rotate a webhook secret without downtime?","Use a provider that supports overlap. During rotation the provider signs each delivery with both the old and the new secret, or your endpoint accepts either secret for a short window. Deploy the new secret, confirm deliveries verify with it, then retire the old one.",{"type":57,"children":58,"toc":602},"root",[59,95,168,175,209,214,220,340,345,351,356,414,419,425,469,475,480,514,519,525,578,596],{"type":60,"tag":61,"props":62,"children":63},"element","p",{},[64,71,73,78,80,85,87,93],{"type":60,"tag":65,"props":66,"children":67},"strong",{},[68],{"type":69,"value":70},"text","A webhook secret, also called a signing secret, is a random key shared between a webhook provider and your endpoint.",{"type":69,"value":72}," The provider uses it to compute an ",{"type":60,"tag":74,"props":75,"children":76},"a",{"href":30},[77],{"type":69,"value":29},{"type":69,"value":79}," ",{"type":60,"tag":74,"props":81,"children":82},{"href":27},[83],{"type":69,"value":84},"signature",{"type":69,"value":86}," for every delivery, and your ",{"type":60,"tag":74,"props":88,"children":90},{"href":89},"/glossary/webhook-endpoint/",[91],{"type":69,"value":92},"endpoint",{"type":69,"value":94}," uses the same key to recompute that signature. If they match, the request came from someone who knows the secret and the body wasn't changed.",{"type":60,"tag":96,"props":97,"children":101},"pre",{"className":98,"code":99,"language":100,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark","# .env\nSTRIPE_WEBHOOK_SECRET=whsec_5a1b9c...        # issued by Stripe, one per endpoint\nGITHUB_WEBHOOK_SECRET=9f2c4e7a1b...          # chosen by you when adding the webhook\n","bash",[102],{"type":60,"tag":103,"props":104,"children":105},"code",{"__ignoreMap":7},[106,118,145],{"type":60,"tag":107,"props":108,"children":111},"span",{"class":109,"line":110},"line",1,[112],{"type":60,"tag":107,"props":113,"children":115},{"style":114},"--shiki-default:#6A737D",[116],{"type":69,"value":117},"# .env\n",{"type":60,"tag":107,"props":119,"children":121},{"class":109,"line":120},2,[122,128,134,140],{"type":60,"tag":107,"props":123,"children":125},{"style":124},"--shiki-default:#E1E4E8",[126],{"type":69,"value":127},"STRIPE_WEBHOOK_SECRET",{"type":60,"tag":107,"props":129,"children":131},{"style":130},"--shiki-default:#F97583",[132],{"type":69,"value":133},"=",{"type":60,"tag":107,"props":135,"children":137},{"style":136},"--shiki-default:#9ECBFF",[138],{"type":69,"value":139},"whsec_5a1b9c...",{"type":60,"tag":107,"props":141,"children":142},{"style":114},[143],{"type":69,"value":144},"        # issued by Stripe, one per endpoint\n",{"type":60,"tag":107,"props":146,"children":148},{"class":109,"line":147},3,[149,154,158,163],{"type":60,"tag":107,"props":150,"children":151},{"style":124},[152],{"type":69,"value":153},"GITHUB_WEBHOOK_SECRET",{"type":60,"tag":107,"props":155,"children":156},{"style":130},[157],{"type":69,"value":133},{"type":60,"tag":107,"props":159,"children":160},{"style":136},[161],{"type":69,"value":162},"9f2c4e7a1b...",{"type":60,"tag":107,"props":164,"children":165},{"style":114},[166],{"type":69,"value":167},"          # chosen by you when adding the webhook\n",{"type":60,"tag":169,"props":170,"children":172},"h2",{"id":171},"how-a-webhook-secret-is-used",[173],{"type":69,"value":174},"How a webhook secret is used",{"type":60,"tag":176,"props":177,"children":178},"ol",{},[179,191,204],{"type":60,"tag":180,"props":181,"children":182},"li",{},[183,185,189],{"type":69,"value":184},"When you create the webhook, the provider generates a secret (Stripe, ",{"type":60,"tag":74,"props":186,"children":187},{"href":33},[188],{"type":69,"value":32},{"type":69,"value":190}," providers) or you supply one (GitHub, many self-hosted tools).",{"type":60,"tag":180,"props":192,"children":193},{},[194,196,202],{"type":69,"value":195},"For each delivery, the provider signs the body with it: ",{"type":60,"tag":103,"props":197,"children":199},{"className":198},[],[200],{"type":69,"value":201},"HMAC-SHA256(secret, signed_string)",{"type":69,"value":203},".",{"type":60,"tag":180,"props":205,"children":206},{},[207],{"type":69,"value":208},"Your endpoint loads the same secret from its configuration, recomputes the signature over the raw body, and compares.",{"type":60,"tag":61,"props":210,"children":211},{},[212],{"type":69,"value":213},"The secret itself is never sent with the request. Only the signature is.",{"type":60,"tag":169,"props":215,"children":217},{"id":216},"secret-formats",[218],{"type":69,"value":219},"Secret formats",{"type":60,"tag":221,"props":222,"children":223},"table",{},[224,248],{"type":60,"tag":225,"props":226,"children":227},"thead",{},[228],{"type":60,"tag":229,"props":230,"children":231},"tr",{},[232,238,243],{"type":60,"tag":233,"props":234,"children":235},"th",{},[236],{"type":69,"value":237},"Provider",{"type":60,"tag":233,"props":239,"children":240},{},[241],{"type":69,"value":242},"Format",{"type":60,"tag":233,"props":244,"children":245},{},[246],{"type":69,"value":247},"How it's used as the HMAC key",{"type":60,"tag":249,"props":250,"children":251},"tbody",{},[252,275,293,310],{"type":60,"tag":229,"props":253,"children":254},{},[255,261,270],{"type":60,"tag":256,"props":257,"children":258},"td",{},[259],{"type":69,"value":260},"Stripe",{"type":60,"tag":256,"props":262,"children":263},{},[264],{"type":60,"tag":103,"props":265,"children":267},{"className":266},[],[268],{"type":69,"value":269},"whsec_...",{"type":60,"tag":256,"props":271,"children":272},{},[273],{"type":69,"value":274},"The whole string, prefix included",{"type":60,"tag":229,"props":276,"children":277},{},[278,283,288],{"type":60,"tag":256,"props":279,"children":280},{},[281],{"type":69,"value":282},"GitHub",{"type":60,"tag":256,"props":284,"children":285},{},[286],{"type":69,"value":287},"Any string you choose",{"type":60,"tag":256,"props":289,"children":290},{},[291],{"type":69,"value":292},"As-is",{"type":60,"tag":229,"props":294,"children":295},{},[296,301,306],{"type":60,"tag":256,"props":297,"children":298},{},[299],{"type":69,"value":300},"Shopify",{"type":60,"tag":256,"props":302,"children":303},{},[304],{"type":69,"value":305},"The app's client secret",{"type":60,"tag":256,"props":307,"children":308},{},[309],{"type":69,"value":292},{"type":60,"tag":229,"props":311,"children":312},{},[313,317,328],{"type":60,"tag":256,"props":314,"children":315},{},[316],{"type":69,"value":32},{"type":60,"tag":256,"props":318,"children":319},{},[320,326],{"type":60,"tag":103,"props":321,"children":323},{"className":322},[],[324],{"type":69,"value":325},"whsec_",{"type":69,"value":327}," + base64",{"type":60,"tag":256,"props":329,"children":330},{},[331,333,338],{"type":69,"value":332},"Strip ",{"type":60,"tag":103,"props":334,"children":336},{"className":335},[],[337],{"type":69,"value":325},{"type":69,"value":339},", then base64-decode the rest",{"type":60,"tag":61,"props":341,"children":342},{},[343],{"type":69,"value":344},"That last row catches people out. The Standard Webhooks key is the decoded bytes, not the text after the prefix.",{"type":60,"tag":169,"props":346,"children":348},{"id":347},"generating-your-own-secret",[349],{"type":69,"value":350},"Generating your own secret",{"type":60,"tag":61,"props":352,"children":353},{},[354],{"type":69,"value":355},"When the provider asks you to choose one, generate at least 32 random bytes:",{"type":60,"tag":96,"props":357,"children":359},{"className":98,"code":358,"language":100,"meta":7,"style":7},"openssl rand -hex 32\n# or\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n",[360],{"type":60,"tag":103,"props":361,"children":362},{"__ignoreMap":7},[363,388,396],{"type":60,"tag":107,"props":364,"children":365},{"class":109,"line":110},[366,372,377,383],{"type":60,"tag":107,"props":367,"children":369},{"style":368},"--shiki-default:#B392F0",[370],{"type":69,"value":371},"openssl",{"type":60,"tag":107,"props":373,"children":374},{"style":136},[375],{"type":69,"value":376}," rand",{"type":60,"tag":107,"props":378,"children":380},{"style":379},"--shiki-default:#79B8FF",[381],{"type":69,"value":382}," -hex",{"type":60,"tag":107,"props":384,"children":385},{"style":379},[386],{"type":69,"value":387}," 32\n",{"type":60,"tag":107,"props":389,"children":390},{"class":109,"line":120},[391],{"type":60,"tag":107,"props":392,"children":393},{"style":114},[394],{"type":69,"value":395},"# or\n",{"type":60,"tag":107,"props":397,"children":398},{"class":109,"line":147},[399,404,409],{"type":60,"tag":107,"props":400,"children":401},{"style":368},[402],{"type":69,"value":403},"node",{"type":60,"tag":107,"props":405,"children":406},{"style":379},[407],{"type":69,"value":408}," -e",{"type":60,"tag":107,"props":410,"children":411},{"style":136},[412],{"type":69,"value":413}," \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n",{"type":60,"tag":61,"props":415,"children":416},{},[417],{"type":69,"value":418},"Don't use a password, a word or anything you've used elsewhere.",{"type":60,"tag":169,"props":420,"children":422},{"id":421},"storing-a-webhook-secret",[423],{"type":69,"value":424},"Storing a webhook secret",{"type":60,"tag":426,"props":427,"children":428},"ul",{},[429,439,449,459],{"type":60,"tag":180,"props":430,"children":431},{},[432,437],{"type":60,"tag":65,"props":433,"children":434},{},[435],{"type":69,"value":436},"Keep it out of code.",{"type":69,"value":438}," Load it from an environment variable or a secret manager such as AWS Secrets Manager, GCP Secret Manager, Vault or Doppler.",{"type":60,"tag":180,"props":440,"children":441},{},[442,447],{"type":60,"tag":65,"props":443,"children":444},{},[445],{"type":69,"value":446},"One secret per endpoint and environment.",{"type":69,"value":448}," Separate secrets for production, staging and local development mean a leaked dev secret can't be used to forge production events.",{"type":60,"tag":180,"props":450,"children":451},{},[452,457],{"type":60,"tag":65,"props":453,"children":454},{},[455],{"type":69,"value":456},"Never log it.",{"type":69,"value":458}," Don't print it in errors, and be careful with request-logging middleware that dumps configuration.",{"type":60,"tag":180,"props":460,"children":461},{},[462,467],{"type":60,"tag":65,"props":463,"children":464},{},[465],{"type":69,"value":466},"Restrict access.",{"type":69,"value":468}," Only the service that verifies webhooks needs it.",{"type":60,"tag":169,"props":470,"children":472},{"id":471},"rotating-a-webhook-secret",[473],{"type":69,"value":474},"Rotating a webhook secret",{"type":60,"tag":61,"props":476,"children":477},{},[478],{"type":69,"value":479},"Rotate on a schedule and immediately if a secret leaks. To avoid rejecting legitimate deliveries mid-rotation:",{"type":60,"tag":176,"props":481,"children":482},{},[483,504,509],{"type":60,"tag":180,"props":484,"children":485},{},[486,488,494,496,502],{"type":69,"value":487},"Generate the new secret at the provider. Good providers then sign with both secrets for a while. The Webhook Relay ",{"type":60,"tag":74,"props":489,"children":491},{"href":490},"/outbound-webhooks/",[492],{"type":69,"value":493},"Outbound Gateway",{"type":69,"value":495},", for example, signs every delivery with both the new and previous secret for 24 hours, so the ",{"type":60,"tag":103,"props":497,"children":499},{"className":498},[],[500],{"type":69,"value":501},"webhook-signature",{"type":69,"value":503}," header carries two signatures.",{"type":60,"tag":180,"props":505,"children":506},{},[507],{"type":69,"value":508},"Deploy the new secret to your endpoint, or accept either secret during the overlap.",{"type":60,"tag":180,"props":510,"children":511},{},[512],{"type":69,"value":513},"Confirm deliveries verify with the new secret, then remove the old one.",{"type":60,"tag":61,"props":515,"children":516},{},[517],{"type":69,"value":518},"If the provider supports only one secret at a time, make your verifier accept a list of secrets, add the new one, rotate at the provider, then remove the old one.",{"type":60,"tag":169,"props":520,"children":522},{"id":521},"common-mistakes",[523],{"type":69,"value":524},"Common mistakes",{"type":60,"tag":426,"props":526,"children":527},{},[528,538,548,558,568],{"type":60,"tag":180,"props":529,"children":530},{},[531,536],{"type":60,"tag":65,"props":532,"children":533},{},[534],{"type":69,"value":535},"Using the wrong environment's secret.",{"type":69,"value":537}," Stripe test-mode and live-mode secrets differ, and every Stripe endpoint has its own. A mismatch makes every signature fail.",{"type":60,"tag":180,"props":539,"children":540},{},[541,546],{"type":60,"tag":65,"props":542,"children":543},{},[544],{"type":69,"value":545},"Trailing whitespace or quotes.",{"type":69,"value":547}," A newline copied into an environment variable silently changes the key.",{"type":60,"tag":180,"props":549,"children":550},{},[551,556],{"type":60,"tag":65,"props":552,"children":553},{},[554],{"type":69,"value":555},"Using the secret text instead of the decoded key",{"type":69,"value":557}," for Standard Webhooks.",{"type":60,"tag":180,"props":559,"children":560},{},[561,566],{"type":60,"tag":65,"props":562,"children":563},{},[564],{"type":69,"value":565},"Sharing one secret across many customers",{"type":69,"value":567}," when you send webhooks. Each customer endpoint should have its own, so one leak doesn't compromise everyone.",{"type":60,"tag":180,"props":569,"children":570},{},[571,576],{"type":60,"tag":65,"props":572,"children":573},{},[574],{"type":69,"value":575},"Treating a secret in the URL as enough.",{"type":69,"value":577}," A token in the query string ends up in access logs and doesn't protect the body from tampering. Sign the body.",{"type":60,"tag":61,"props":579,"children":580},{},[581,583,588,590,594],{"type":69,"value":582},"To check a secret against a captured request, use the ",{"type":60,"tag":74,"props":584,"children":585},{"href":20},[586],{"type":69,"value":587},"signature verifier",{"type":69,"value":589}," or the ",{"type":60,"tag":74,"props":591,"children":592},{"href":42},[593],{"type":69,"value":41},{"type":69,"value":595},". Both run in the browser.",{"type":60,"tag":597,"props":598,"children":599},"style",{},[600],{"type":69,"value":601},"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);}",{"title":7,"searchDepth":147,"depth":147,"links":603},[604,605,606,607,608,609],{"id":171,"depth":120,"text":174},{"id":216,"depth":120,"text":219},{"id":347,"depth":120,"text":350},{"id":421,"depth":120,"text":424},{"id":471,"depth":120,"text":474},{"id":521,"depth":120,"text":524},"markdown","content:glossary:webhook-secret.md","content","glossary/webhook-secret.md","glossary/webhook-secret","md",{"loc":4},1793199805199]