{"record":{"id":"6ee1112a5c872949","repo":"Significant-Gravitas/AutoGPT","slug":"the-chatgpt-verification-link-is-invalid-close-th","errorCode":null,"errorMessage":"The ChatGPT verification link is invalid. Close this window and try again.","messagePattern":"The ChatGPT verification link is invalid\\. Close this window and try again\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"autogpt_platform/backend/backend/api/features/integrations/codex_device_page.py","lineNumber":79,"sourceCode":"        const statusElement = document.getElementById(\"status\");\n        const errorElement = document.getElementById(\"error\");\n        const copyButton = document.getElementById(\"copy-code\");\n\n        function fail(message) {\n          statusElement.textContent = \"\";\n          errorElement.textContent = message;\n        }\n\n        window.addEventListener(\"pagehide\", function () {\n          if (!finished) {\n            navigator.sendBeacon(window.location.pathname + \"/cancel\");\n          }\n        });\n\n        let verificationUrl;\n        try {\n          verificationUrl = new URL(rawVerificationUrl);\n          if (verificationUrl.protocol !== \"https:\") throw new Error();\n        } catch (_) {\n          fail(\"The ChatGPT verification link is invalid. Close this window and try again.\");\n          return;\n        }\n        if (!state || !userCode || !loginID) {\n          fail(\"This sign-in attempt is incomplete. Close this window and try again.\");\n          return;\n        }\n\n        codeElement.textContent = userCode;\n        linkElement.href = verificationUrl.toString();\n        copyButton.addEventListener(\"click\", async function () {\n          try {\n            await navigator.clipboard.writeText(userCode);\n            copyButton.textContent = \"Copied\";\n          } catch (_) {\n            fail(\"Could not copy automatically. Select the code above instead.\");\n          }","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/features/integrations/codex_device_page.py#L61-L97","documentation":"This is an HTTP 401 Unauthorized raised by APIKeyAuthenticator, a FastAPI security dependency that validates an API-key header on incoming requests. The header was present (a missing header produces the distinct 'No API key in request' error at line 86-88), but the validator returned a falsy result: either the custom validator rejected the key, or the default_validator's secrets.compare_digest mismatch against expected_token. In this repo the concrete production use is the Postmark webhook router (backend/api/features/postmark/postmark.py:31), which compares the X-Postmark-Webhook-Token header to settings.secrets.postmark_webhook_token.","triggerScenarios":"Calling POST /api/postmark/ (or any route secured with Security(api_key_auth)) with an X-Postmark-Webhook-Token header whose value does not exactly match the configured token. Also triggered when the server-side token setting is None or empty and compare_digest raises TypeError (line 117-120 logs a warning and returns False), or when the header value contains non-ASCII characters, which likewise makes compare_digest raise TypeError and get converted to a validation failure.","commonSituations":"The POSTMARK_WEBHOOK_TOKEN secret is not set (or is set to a placeholder like 'changeme') in backend/.env on the server while a correct token is configured in Postmark's webhook settings; the token was rotated in Postmark but not in the deployment env; trailing whitespace, quotes, or encoding issues from copy-pasting the token into .env or the Postmark dashboard; sending the token in the wrong header (e.g. Authorization or X-API-Key instead of X-Postmark-Webhook-Token gets the 'No API key' 401 instead); multiple deployments (staging vs prod) with different tokens.","solutions":["Compare the exact token value: check the X-Postmark-Webhook-Token header you are sending against settings.secrets.postmark_webhook_token (usually the POSTMARK_WEBHOOK_TOKEN env var in backend/.env or docker-compose env) — they must match character-for-character with no whitespace or quotes.","If the backend log shows the warning 'API key check failed: ...' from api_key_auth.py:119, the expected token is None/empty or non-ASCII: set a valid non-empty ASCII token in the backend environment (backend/.env → secrets section) and restart the service.","If the token was rotated in the Postmark dashboard, copy the new webhook token into the same env var on every deployment that receives the webhook, then restart.","Verify you are hitting the right environment: staging and production often run different secrets; confirm the URL configured in Postmark points at the deployment whose env you inspected.","For local reproduction, curl -X POST http://localhost:8000/api/postmark/ -H 'X-Postmark-Webhook-Token: <token>' -H 'Content-Type: application/json' -d '{...}' and confirm a non-401 response before re-configuring Postmark."],"exampleFix":"# before: backend/.env has a placeholder or nothing\nPOSTMARK_WEBHOOK_TOKEN=\n\n# after: set the exact token shown in Postmark > Signings > Webhooks (no quotes/spaces)\nPOSTMARK_WEBHOOK_TOKEN=a1b2c3d4e5f6g7h8\n\n# then restart the backend so Settings() picks it up\n# docker compose restart backend   (or: poetry run app)","handlingStrategy":"validation","validationCode":"# Before sending/ configuring, verify the token is usable (server side, Python)\nimport os\n\nPOSTMARK_TOKEN = os.environ.get(\"POSTMARK_WEBHOOK_TOKEN\", \"\")\n\nassert POSTMARK_TOKEN and POSTMARK_TOKEN.isascii() and POSTMARK_TOKEN == POSTMARK_TOKEN.strip(), (\n    \"POSTMARK_WEBHOOK_TOKEN must be a non-empty, ASCII, whitespace-free value\"\n)\n\n# Client side: assert the header you are about to send is non-empty and ASCII\n# before making the request\n# assert token and token.isascii(), 'webhook token missing or non-ASCII'","typeGuard":null,"tryCatchPattern":"# Python client: distinguish 'invalid key' (fix the token) from 'missing key' (fix the header)\nimport requests\n\nresp = requests.post(url, headers={\"X-Postmark-Webhook-Token\": token}, json=payload, timeout=10)\nif resp.status_code == 401:\n    detail = resp.json().get(\"detail\")\n    if detail == \"No API key in request\":\n        raise RuntimeError(\"Header X-Postmark-Webhook-Token is not being sent\") from None\n    if detail == \"Invalid API key\":\n        raise RuntimeError(\"Token mismatch: rotate/re-copy POSTMARK_WEBHOOK_TOKEN\") from None\nresp.raise_for_status()","preventionTips":["Treat the webhook token as provisioned config: set POSTMARK_WEBHOOK_TOKEN in backend/.env (or docker-compose environment) at deploy time and fail startup if it is empty when the Postmark router is enabled.","Never copy tokens with surrounding quotes or spaces; strip them on entry and validate the value is non-empty ASCII.","After any token rotation in the Postmark dashboard, update every environment (staging, prod) that receives webhooks and restart the service, since Settings() reads secrets at startup.","In tests, exercise both failure modes (wrong token -> 401 'Invalid API key', absent header -> 401 'No API key in request') as done in backend/backend/api/utils/api_key_auth_test.py so regressions in header name or comparison are caught early.","Monitor for the 'API key check failed' warning from api_key_auth.py:119 — it indicates a misconfigured (None/non-ASCII) expected token rather than an attacker, so alert on it separately from auth failures."],"tags":["authentication","api-key","fastapi","http-401","webhook","postmark","secrets","env-config"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}