Accept an invitation using its token
POST/api/invitations/:token/accept
Accept an invitation using its token
Request
Responses
- 200
- 400
- 401
- 403
- 404
- 409
- 410
- 500
Successful response
Bad Request — the token is valid and redeemable, but the invitation is malformed: its organisation or project role does not belong to its tenant; its project lies outside that tenant; the tenant is not ACTIVE; or the tenant has no organisation to join. A token that is unknown, already consumed or expired is NOT reported here — see 404 and 410.
Unauthorized — authentication credentials are missing or invalid
Forbidden. The primary cause on this operation is that the authenticated caller is not the invitation recipient — the token is valid, but its email does not match theirs (compared case-insensitively). This is what stops a leaked token being redeemed by whoever holds it. The generic x-act-as-org cause applies here too, as it does to every authed operation.
Not Found — no invitation matches this token. Narrower than it looks: a token that exists but was issued to a different email address is a 403, not a 404, so this status means the token itself is unknown.
Conflict — the grant refuses to complete. Two causes. (1) The invitation stopped being PENDING between this route reading it and the grant claiming it, so the guarded compare-and-set matched no row: "Invitation is no longer pending". This is the race; the settled equivalent is a 410 above. (2) A collaborator record for this user and project exists outside the invitation's tenant, reported with a deliberately non-specific message, since naming the obstruction would disclose that a row exists in a tenant the caller cannot see. Only (1) is worth retrying, and only with a reissued invitation; (2) needs an operator to resolve the row.
Gone — the invitation existed but can no longer be redeemed, and reissuing is the only remedy. Two cases: it is no longer PENDING (already accepted, declined or revoked), or it has passed expiresAt — the latter also transitions the row to EXPIRED on the way out. Distinct from 409, which is a race against a concurrent change rather than a settled state observed up front.
Internal Server Error, and two of the causes are DELIBERATE rather than the catch-all every route carries. UNEXPECTED_ROW_SHAPE: a raw-SQL read behind the grant returned a shape that does not match its statement, which is parsed rather than cast (PT-888) so it fails loudly instead of becoming a tenancy decision. IDENTITY_GRANT_FAILED: the Directory-owned identity boundary function reported no ACTIVE membership after being asked to grant one, or returned a shape this module does not understand. Unlike the unauthenticated registration route, which collapses its 500s to a generic message, this route is authenticated and serves the AppError envelope as-is — so a client does receive the distinct code and a message naming which read drifted. What stays server-side is the field-level detail: the failing column, and the boundary text carrying function names and tenant identifiers. Neither cause is retryable by the caller.