POST /api/organizations/[login]/mass-invite
File: app/api/organizations/[login]/mass-invite/route.ts (67 lines)
Core logic: lib/auth/github.ts:326-416 (inviteUsersToOrganization)
| Param | Type | Description |
|---|---|---|
login | string | GitHub organization login |
Route param accessed via params.login (Next.js 16 Promise-based).
Requires an active session + you must be an org admin (viewerCanAdminister === true).
Validation pipeline
Phase 1: Request validation
| Check | Message | Status |
|---|---|---|
| Session exists | ”Please sign in to continue.” | 401 |
login is non-empty | ”Please select an organization first.” | 400 |
| Valid JSON body | ”Please add at least one user to invite.” | 400 |
userLogins is a non-empty array | ”Please add at least one user to invite.” | 400 |
| All entries are strings | ”Invalid user data. Please remove and re-add the users.” | 400 |
| Non-empty after trim+dedup+filter | ”Please add at least one user to invite.” | 400 |
| ≤ 50 unique after dedup | ”You can invite up to 50 users at a time.” | 413 |
| You’re an org admin | ”Only organization admins can send invitations.” | 403 |
Phase 2: Normalization
const normalizedLogins = userLogins.map(l => l.trim()).filter(l => l.length > 0);
const uniqueLogins = [...new Set(normalizedLogins)];[" user1 ", "user1", "USER1", ""] → ["user1", "USER1"]. Note: GitHub usernames are case-sensitive.
Phase 3: Admin check
session.user.organizations.some(org => org.login === login && org.viewerCanAdminister);Uses org data from session creation time. If you were granted admin rights after signing in, you’ll need to re-authenticate.
Invite execution
const CONCURRENCY = 5;
await Promise.all(batch.map(login => processInvite(login)));Each user goes through two steps, both within a concurrency-limited batch:
Step 1: GraphQL user lookup (lib/auth/github.ts:341-349)
query GetUserByLogin($login: String!) {
user(login: $login) {
databaseId
}
}Returns null if the user doesn’t exist (→ "User @{login} not found on GitHub"). Uses the session’s OAuth token.
Step 2: REST invitation (lib/auth/github.ts:352-355)
POST /orgs/{org}/invitations
Body: { invitee_id: <databaseId> }There’s no GraphQL mutation for org invitations, so this has to use REST.
Rate limiting
GitHub’s rate limit is 5,000 requests/hour. Each invite = 2 requests (1 GraphQL + 1 REST). At 50 users max, that’s 100 requests. With concurrency set to 5, you get roughly 20 sequential batches. Well within limits for a single operation.
Response
{
"success": ["user1", "user2"],
"failed": [{ "login": "user3", "error": "User @user3 not found on GitHub" }]
}interface MassInviteResult {
success: string[];
failed: Array<{ login: string; error: string }>;
}Both arrays are always present. They’ll be empty on their respective extremes.
Error response
{ "error": "Failed to send invitations. Please try again." }Per-user error handling
| Scenario | Error message | Capture point |
|---|---|---|
| User not found | "User @{login} not found on GitHub" | GraphQL null check |
| GitHub API error (already member, suspended) | GitHub’s API message | REST try-catch: err.response?.data?.message |
| Rate limited | "API rate limit exceeded" | REST try-catch |
| Network error | Node.js error message | Call try-catch |
| Unknown | "Unknown error" | Catch-all |
Common GitHub messages you’ll see:
"Invitations have been disabled for this organization""invitee_id is already a member of this organization""User has been suspended"
Edge cases
- Rate limit hit mid-batch: partial results. No retry mechanism.
- User already a member: captured in
failedwith GitHub’s message. You can distinguish soft errors (already a member) from hard ones (not found). - Org disabled invitations: all invites fail with GitHub’s message.
- Username case:
new Set()is case-sensitive."User"and"user"are treated as different people. - Admin staleness: session org data comes from sign-in time. New admins need to re-authenticate.
- OAuth token expiry: the session cookie might still be valid (24h TTL) but the underlying token could expire. All invites would 401. The frontend should detect this and prompt re-auth.
Related
- Organization Routes — org list and detail endpoints
- Auth Routes — session authentication
- GraphQL API — user lookup queries