Subchapter 9.2
references/invitations.mdMarkdown6 KBView on GitHub
Send, list, revoke. Backend API methods live on clerkClient().organizations.*. All send operations require the caller to have org:sys_memberships:manage.
Framework wrappers. Method signatures on
clerk.organizations.*are identical across SDKs; only the wrapper that gives you the client differs:
| SDK | Get the client | Get the auth context |
|---|---|---|
@clerk/nextjs/server | const clerk = await clerkClient() | const { userId, has } = await auth() |
@clerk/backend (agnostic) | const clerk = createClerkClient({ secretKey }) | n/a (verify the session token yourself with verifyToken imported from @clerk/backend) |
@clerk/astro/server | const clerk = clerkClient(context) | const { userId } = context.locals.auth() |
@clerk/nuxt/server | const clerk = clerkClient(event) | const { userId } = event.context.auth() |
@clerk/express | const clerk = clerkClient (after clerkMiddleware()) | const { userId } = getAuth(req) |
Examples below use @clerk/nextjs as the default flavor.
import { clerkClient, auth } from '@clerk/nextjs/server'
export async function inviteMember(organizationId: string, emailAddress: string, role: string) {
const { userId, has } = await auth()
if (!userId) throw new Error('Not signed in')
if (!has({ permission: 'org:sys_memberships:manage' })) {
throw new Error('Not authorized')
}
const clerk = await clerkClient()
return clerk.organizations.createOrganizationInvitation({
organizationId,
inviterUserId: userId,
emailAddress,
role,
redirectUrl: 'https://yourapp.com/accept-invite',
publicMetadata: { invitedFrom: 'admin-panel' },
})
}Params:
| Param | Type | Notes |
|---|---|---|
organizationId | string | Required |
inviterUserId | string | null | Required. The user sending the invite. Pass null only for system-originated invites (rare). |
emailAddress | string | Required. Target email. |
role | string | Required. 'org:admin', 'org:member', or any custom role slug. |
redirectUrl? | string | Where the user lands after accepting. |
publicMetadata? | object | Readable by Frontend + Backend; settable only from Backend. |
Rate limit: 250 requests/hour per application instance.
Takes the organizationId as its first positional arg and an array of per-invitation params as its second:
await clerk.organizations.createOrganizationInvitationBulk(organizationId, [
{ inviterUserId: userId, emailAddress: 'alice@acme.com', role: 'org:admin' },
{ inviterUserId: userId, emailAddress: 'bob@acme.com', role: 'org:member' },
])Each item accepts the same optional fields as a single createOrganizationInvitation call (redirectUrl, publicMetadata). The bulk endpoint is rate-limited separately at 50 requests/hour per application instance (vs 250/hr for single create).
const { data, totalCount } = await clerk.organizations.getOrganizationInvitationList({
organizationId,
status: ['pending', 'accepted', 'revoked', 'expired'], // any subset; defaults to ['pending']
limit: 50, // max 500
offset: 0,
})Returns a PaginatedResourceResponse<OrganizationInvitation[]> — access the array via data and the total via totalCount.
Full status enum: 'pending' | 'accepted' | 'revoked' | 'expired'. Skipping status defaults to ['pending'].
await clerk.organizations.revokeOrganizationInvitation({
organizationId,
invitationId,
requestingUserId: userId, // the user doing the revoking
})All three params are required strings. You cannot revoke an already-accepted invitation (use membership removal APIs for that).
const invitation = await clerk.organizations.getOrganizationInvitation({
organizationId,
invitationId,
})Zero-code path — <OrganizationProfile /> includes a full members tab with invite / revoke / role change:
import { OrganizationProfile } from '@clerk/nextjs'
export default function OrgSettings() {
return <OrganizationProfile />
}<OrganizationSwitcher /> also includes a compact invitation flow via its built-in dropdown when hidePersonal is set or users click Manage Organization:
<OrganizationSwitcher
hidePersonal
afterCreateOrganizationUrl="/orgs/:slug/dashboard"
afterSelectOrganizationUrl="/orgs/:slug/dashboard"
/>If you need to build your own accept page instead of relying on Clerk’s account portal, see the custom flow doc: Accept Organization Invitations (opens in a new tab). Common pattern:
/accept-invite page with ?__clerk_ticket=... query paramsignIn.create({ strategy: 'ticket', ticket }) OR signUp.create({ strategy: 'ticket', ticket }) depending on whether the user existsListen for invitation lifecycle:
organizationInvitation.createdorganizationInvitation.acceptedorganizationInvitation.revokedSee clerk-webhooks skill for webhook setup + signature verification.
inviterUserId is NOT optional in a human-initiated flow. Don’t omit it — track who sent each invite.status: 'expired' need to be recreated; they can’t be re-sent.org:sys_memberships:manage. Default org:admin has this; org:member does not.invitationId, not by email. Email alone is ambiguous when you’ve had multiple invites to the same address.createOrganizationInvitation is 250/hr, createOrganizationInvitationBulk is 50/hr. Batch wisely.