{"record":{"id":"11868d80542ebed6","repo":"openai/openai-python","slug":"webhook-timestamp-is-too-old","errorCode":null,"errorMessage":"Webhook timestamp is too old","messagePattern":"Webhook timestamp is too old","errorType":"validation","errorClass":"InvalidWebhookSignatureError","httpStatus":null,"severity":"error","filePath":"src/openai/lib/_webhooks.py","lineNumber":34,"sourceCode":"    *,\n    secret: str,\n    tolerance: int,\n) -> bool:\n    \"\"\"Validate the replay window and compare the supplied signatures.\"\"\"\n    signature_header = get_required_header(headers, \"webhook-signature\")\n    timestamp = get_required_header(headers, \"webhook-timestamp\")\n    webhook_id = get_required_header(headers, \"webhook-id\")\n\n    # Validate timestamp to prevent replay attacks\n    try:\n        timestamp_seconds = int(timestamp)\n    except ValueError:\n        raise InvalidWebhookSignatureError(\"Invalid webhook timestamp format\") from None\n\n    now = int(time.time())\n\n    if now - timestamp_seconds > tolerance:\n        raise InvalidWebhookSignatureError(\"Webhook timestamp is too old\") from None\n\n    if timestamp_seconds > now + tolerance:\n        raise InvalidWebhookSignatureError(\"Webhook timestamp is too new\") from None\n\n    # Extract signatures from v1,<base64> format\n    # The signature header can have multiple values, separated by spaces.\n    # Each value is in the format v1,<base64>. We should accept if any match.\n    signatures: list[str] = []\n    for part in signature_header.split():\n        if part.startswith(\"v1,\"):\n            signatures.append(part[3:])\n        else:\n            signatures.append(part)\n\n    # Decode the secret if it starts with whsec_\n    if secret.startswith(\"whsec_\"):\n        decoded_secret = base64.b64decode(secret[6:])\n    else:","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/openai/openai-python/blob/9917c6e28e66e90e1227b3d223c06a8c5441515a/src/openai/lib/_webhooks.py#L16-L52","documentation":"Anti-replay protection in webhook_signature_matches: the signed webhook-timestamp is older than the allowed tolerance (default 5 minutes) relative to server time, so the signature is rejected as a possible replay.","triggerScenarios":"Receiving a webhook more than tolerance seconds after it was signed; also caused by server clock skew, or replaying/queuing old webhook deliveries.","commonSituations":"Queue backlog delaying delivery; NTP drift on the receiving host; processing retries of old requests; deliberately re-sending captured payloads.","solutions":["Sync the receiving server's clock (NTP/chrony)","If you legitimately process delayed deliveries (queues), pass a larger tolerance in seconds to webhook_signature_matches","Do not re-process captured old requests; fetch fresh state instead"],"exampleFix":"# before\nwebhook_signature_matches(body, headers, secret)\n# after\nwebhook_signature_matches(body, headers, secret, tolerance=3600)","handlingStrategy":"try-catch","validationCode":"ts = int(headers[\"webhook-timestamp\"])\nimport time\nif time.time() - ts > tolerance:\n    # queue-delayed webhook: widen tolerance or fetch fresh state","typeGuard":null,"tryCatchPattern":"try:\n    ok = webhook_signature_matches(body, headers, secret, tolerance=3600)\nexcept InvalidWebhookSignatureError as e:\n    if \"too old\" in str(e):\n        return Response(408)\n    raise","preventionTips":["Run NTP on webhook receivers","Size tolerance to your delivery pipeline latency","Re-fetch state instead of replaying old webhook bodies"],"tags":["webhooks","replay-attack","timestamp","clock-skew"],"backgroundTag":"webhook-timestamp-expired","analyzedSha":"9917c6e28e66e90e1227b3d223c06a8c5441515a","analyzedAt":"2026-08-28T11:46:34.183Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}