Organization Invitations
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/serverconst clerk = await clerkClient()const { userId, has } = await auth()@clerk/backend(agnostic)const clerk = createClerkClient({ secretKey })n/a (verify the session token yourself with verifyTokenimported from@clerk/backend)@clerk/astro/serverconst clerk = clerkClient(context)const { userId } = context.locals.auth()@clerk/nuxt/serverconst clerk = clerkClient(event)const { userId } = event.context.auth()@clerk/expressconst clerk = clerkClient(afterclerkMiddleware())const { userId } = getAuth(req)Examples below use
@clerk/nextjsas the default flavor.
Create Invitation
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.
Bulk Create
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).
List Invitations
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'].
Revoke Invitation
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).
Get a Single Invitation
const invitation = await clerk.organizations.getOrganizationInvitation({
organizationId,
invitationId,
})Built-in Invitation UI
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"
/>Accept Invitations (Custom Flow)
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. Common pattern:
- User clicks link → lands on your
/accept-invitepage with?__clerk_ticket=...query param - Your page calls
signIn.create({ strategy: 'ticket', ticket })ORsignUp.create({ strategy: 'ticket', ticket })depending on whether the user exists - Clerk sets the active org on the session
Webhook Events
Listen for invitation lifecycle:
organizationInvitation.createdorganizationInvitation.acceptedorganizationInvitation.revoked
See clerk-webhooks skill for webhook setup + signature verification.
Key Rules
inviterUserIdis NOT optional in a human-initiated flow. Don't omit it — track who sent each invite.- Invitations with
status: 'expired'need to be recreated; they can't be re-sent. - The caller needs
org:sys_memberships:manage. Defaultorg:adminhas this;org:memberdoes not. - Revoke by
invitationId, not by email. Email alone is ambiguous when you've had multiple invites to the same address. - Rate limits differ by endpoint: single
createOrganizationInvitationis 250/hr,createOrganizationInvitationBulkis 50/hr. Batch wisely.