{"id":"webhook-stripe-verify","version":"1.0.0","description":"Verify a Stripe webhook's Stripe-Signature header against a raw payload, with secret-rotation and tolerance support.","supported_operations":["verify stripe webhook signature","check stripe-signature header","validate an incoming stripe event","is this webhook really from stripe","stripe signing secret manual verification","verify a rotated stripe secret against multiple v1 signatures"],"unsupported_operations":["computing a fresh Stripe-Signature header for a test delivery (see webhook-signature-compute)","verifying any other vendor webhook scheme (see webhook-slack-verify, webhook-standard-verify, webhook-twilio-verify)"],"semantics":["signature_header is split on commas into 'key=value' pairs. The FIRST 't=' pair's value is parsed as a decimal integer -> timestamp (a header with no 't=' pair, or whose value is not a valid decimal integer, is reason 'header_unparsable', timestamp null, matched_scheme null). Every 'v1=' pair's value is collected, in order, as candidate signatures; a header with a valid timestamp but zero v1= pairs is reason 'no_v1_signature' (matched_scheme null; a 'v0=' or other scheme is simply not collected, not an error by itself).","The signed string is the CANONICAL decimal string of the parsed timestamp integer (e.g. a header with 't=007' parses to the same signed string as 't=7') followed by a literal '.' followed by payload verbatim (raw text, never re-serialized).","expected = lowercase-hex(HMAC-SHA256(secret as UTF-8 bytes verbatim -- including its 'whsec_' prefix, never stripped -- signed string as UTF-8 bytes)). The tool constant-time-compares expected against EVERY collected v1 candidate; any one match is sufficient (secret rotation). No match is reason 'signature_mismatch', matched_scheme still 'v1'.","Tolerance is ONE-SIDED: when now is given and tolerance_seconds > 0, the check is (now - timestamp) > tolerance_seconds -- a timestamp in the FUTURE never fails this check, no matter how far in the future. This is checked only after a signature match succeeds. tolerance_seconds 0 or now omitted -> timestamp_checked false, no freshness check at all.","matched_scheme is 'v1' whenever header parsing got far enough to attempt a signature comparison (reason signature_mismatch, timestamp_outside_tolerance, or valid true); it is null only for header_unparsable or no_v1_signature.","secret is never echoed in the output or in any error details."],"limits":{"max_payload_bytes":262144},"pricing":{"status":"unpriced","charge_usd":null},"input_schema":{"type":"object","additionalProperties":false,"required":["payload","signature_header","secret"],"properties":{"payload":{"type":"string","maxLength":262144},"signature_header":{"type":"string","minLength":1,"maxLength":4096},"secret":{"type":"string","minLength":1,"maxLength":512},"now":{"type":"integer","minimum":0,"maximum":99999999999},"tolerance_seconds":{"type":"integer","minimum":0,"maximum":86400,"default":300}}},"output_schema":{"type":"object","additionalProperties":false,"required":["valid","reason","timestamp","timestamp_checked","matched_scheme"],"properties":{"valid":{"type":"boolean"},"reason":{"type":["string","null"],"enum":["header_unparsable","no_v1_signature","signature_mismatch","timestamp_outside_tolerance",null]},"timestamp":{"type":["integer","null"]},"timestamp_checked":{"type":"boolean"},"matched_scheme":{"type":["string","null"],"enum":["v1",null]}}},"examples":[{"input":{"payload":"{\"id\":\"evt_1\"}","signature_header":"t=1614000000,v1=e6bce1af69d4a58ca43b91bd6c94e6cccc1c8a1cbf68d6dc6a2e1fbdc4536dd2","secret":"whsec_test_secret"},"output":{"valid":false,"reason":"signature_mismatch","timestamp":1614000000,"timestamp_checked":false,"matched_scheme":"v1"}},{"input":{"payload":"{\"id\":\"evt_1\"}","signature_header":"v1=deadbeef","secret":"whsec_test_secret"},"output":{"valid":false,"reason":"header_unparsable","timestamp":null,"timestamp_checked":false,"matched_scheme":null}}],"execute_url":"/v1/tools/webhook-stripe-verify/versions/1.0.0/execute"}