Enterprise access patterns for ClickUp API v2. ClickUp's role system is built into the workspace, and the API surfaces roles via member objects. OAuth 2.0 enables multi-workspace apps where each user authorizes their own workspaces.
ClickUp workspace members have role IDs in the API:
| Role ID | Role | Permissions |
|---|---|---|
| 1 | Owner | Full control, billing, workspace settings |
| 2 | Admin | Manage members, spaces, integrations |
| 3 | Member | Create/edit tasks, spaces (per permission) |
| 4 | Guest | Limited access to shared items only |
// Get workspace members with roles
async function getWorkspaceMembers(teamId: string) {
const data = await clickupRequest(`/team/${teamId}`);
return data.team.members.map((m: any) => ({
userId: m.user.id,
username: m.user.username,
email: m.user.email,
role: m.user.role, // 1=owner, 2=admin, 3=member, 4=guest
roleLabel: { 1: 'owner', 2: 'admin', 3: 'member', 4: 'guest' }[m.user.role],
}));
}
// Check if user can perform admin operations
function canAdminister(member: { role: number }): boolean {
return member.role <= 2; // Owner or Admin
}
Build apps that access multiple ClickUp workspaces on behalf of users.
// Step 1: Redirect user to ClickUp authorization
function getOAuthUrl(state: string): string {
return `https://app.clickup.com/api?client_id=${process.env.CLICKUP_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.CLICKUP_REDIRECT_URI!)}&state=${state}`;
}
// Step 2: Exchange code for token
async function handleOAuthCallback(code: string) {
const response = await fetch('https://api.clickup.com/api/v2/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.CLICKUP_CLIENT_ID,
client_secret: process.env.CLICKUP_CLIENT_SECRET,
code,
}),
});
const { access_token } = await response.json();
// Step 3: Discover which workspaces user authorized
const teamsResponse = await fetch('https://api.clickup.com/api/v2/team', {
headers: { 'Authorization': access_token },
});
const { teams } = await teamsResponse.json();
return {
token: access_token, // Doesn't expire (but can be revoked)
workspaces: teams.map((t: any) => ({ id: t.id, name: t.name })),
};
}
// Step 4: Store per-user tokens
interface UserClickUpAuth {
userId: string;
clickupToken: string; // Encrypt at rest
authorizedWorkspaces: string[];
connectedAt: Date;
}
// Express middleware that checks ClickUp workspace access
function requireClickUpAccess(requiredRole: number = 3) {
return async (req: any, res: any, next: any) => {
const userToken = req.user.clickupToken;
const teamId = req.params.teamId || req.body.teamId;
if (!userToken) {
return res.status(401).json({ error: 'ClickUp not connected' });
}
// Verify user still has access to this workspace
const teamsRes = await fetch('https://api.clickup.com/api/v2/team', {
headers: { 'Authorization': userToken },
});
if (!teamsRes.ok) {
return res.status(401).json({ error: 'ClickUp token expired or revoked' });
}
const { teams } = await teamsRes.json();
const workspace = teams.find((t: any) => t.id === teamId);
if (!workspace) {
return res.status(403).json({ error: 'No access to this ClickUp workspace' });
}
// Check role level
const userMember = workspace.members.find(
(m: any) => m.user.id === req.user.clickupUserId
);
if (!userMember || userMember.user.role > requiredRole) {
return res.status(403).json({
error: `Requires role ${requiredRole} or higher`,
});
}
req.clickupWorkspace = workspace;
next();
};
}
// Usage
app.delete('/api/clickup/:teamId/space/:spaceId',
requireClickUpAccess(2), // Admin required
async (req, res) => { /* ... */ }
);
GET /api/v2/group Get User Groups
POST /api/v2/team/{team_id}/group Create User Group
PUT /api/v2/group/{group_id} Update User Group
DELETE /api/v2/group/{group_id} Delete User Group
// Create a user group for engineering team
await clickupRequest(`/team/${teamId}/group`, {
method: 'POST',
body: JSON.stringify({
name: 'Engineering',
member_ids: [183, 456, 789],
}),
});
interface ClickUpAuditEntry {
timestamp: string;
userId: number;
workspaceId: string;
action: string;
resource: string;
resourceId: string;
success: boolean;
}
function logClickUpAction(entry: Omit<ClickUpAuditEntry, 'timestamp'>): void {
const log: ClickUpAuditEntry = {
...entry,
timestamp: new Date().toISOString(),
};
console.log(JSON.stringify({ level: 'audit', service: 'clickup', ...log }));
}
| Issue | Cause | Solution |
|---|---|---|
| OAUTH_023/027 | Workspace not authorized | User must re-authorize via OAuth flow |
| Role check fails | User role changed in ClickUp | Re-fetch member data from API |
| Token revoked | User disconnected app | Handle 401, prompt re-auth |
| Guest access denied | Endpoint requires member+ | Check role field before API call |
For major migrations, see clickup-migration-deep-dive.