{"record":{"id":"b56480a3981e3bec","repo":"Significant-Gravitas/AutoGPT","slug":"invitation-already-accepted","errorCode":null,"errorMessage":"Invitation already accepted","messagePattern":"Invitation already accepted","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"autogpt_platform/backend/backend/api/features/orgs/invitation_routes.py","lineNumber":153,"sourceCode":"\n# --- Token-based endpoints (under /api/invitations) ---\n\n\n@router.post(\n    \"/{token}/accept\",\n    summary=\"Accept invitation\",\n    tags=[\"invitations\"],\n    dependencies=[Security(requires_user)],\n)\nasync def accept_invitation(\n    token: str,\n    user_id: Annotated[str, Security(get_user_id)],\n) -> dict:\n    invitation = await prisma.orginvitation.find_unique(where={\"token\": token})\n    if invitation is None:\n        raise NotFoundError(\"Invitation not found\")\n    if invitation.acceptedAt is not None:\n        raise HTTPException(400, detail=\"Invitation already accepted\")\n    if invitation.revokedAt is not None:\n        raise HTTPException(400, detail=\"Invitation has been revoked\")\n    if invitation.expiresAt < datetime.now(timezone.utc):\n        raise HTTPException(400, detail=\"Invitation has expired\")\n\n    # Verify the accepting user's email matches the invitation\n    accepting_user = await prisma.user.find_unique(where={\"id\": user_id})\n    if accepting_user is None:\n        raise HTTPException(401, detail=\"User not found\")\n    if accepting_user.email.lower() != invitation.email.lower():\n        raise HTTPException(\n            403,\n            detail=\"This invitation was sent to a different email address\",\n        )\n\n    # Add user to org (idempotent — handles race condition from concurrent accepts)\n    try:\n        await org_db.add_org_member(","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/features/orgs/invitation_routes.py#L135-L171","documentation":"Raised by POST /api/invitations/{token}/accept when the invitation exists but acceptedAt is already set — the invitation was consumed by a previous accept. Acceptance is single-use; the membership add itself is idempotent, but the state check rejects a second accept of the same token. HTTP 400.","triggerScenarios":"User clicks the accept link twice (double-click, page reload after accept, email link reopened), or two browser tabs race to accept. The first request sets acceptedAt; subsequent requests get this 400.","commonSituations":"Frontend retries the POST after a network hiccup that actually succeeded; user forwards the email and another acceptance already happened; back-button resubmission.","solutions":["Treat this 400 as success if the user is already a member — verify with GET org members or the pending-invitations endpoint.","Make the client idempotent: disable the accept button on first click and do not auto-retry POSTs.","If UI should allow re-entry, redirect to the org instead of showing an error when this message is returned."],"exampleFix":"// before\nawait api.post(`/api/invitations/${token}/accept`); // throws on second click\n// after\ntry {\n  await api.post(`/api/invitations/${token}/accept`);\n} catch (e) {\n  if (e.status === 400 && e.detail === 'Invitation already accepted') {\n    router.push('/dashboard'); // user is already a member\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const me = await api.get('/api/invitations/pending').then(r => r.data);\nconst stillPending = me.some(i => i.token === token);","typeGuard":null,"tryCatchPattern":"try {\n  await api.post(`/api/invitations/${token}/accept`);\n} catch (e) {\n  if (e.status === 400 && e.detail === 'Invitation already accepted') {\n    router.push('/dashboard'); return; // already a member\n  }\n  throw e;\n}","preventionTips":["Disable the accept button on first click","Never auto-retry non-idempotent POSTs","Check pending-invitations before re-showing accept UI"],"tags":["invitations","state","idempotency","http-400"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}