Better Enrollment

API reference

Every client method, server-only endpoints, and rate limits.

Client methods

Redemption

The two calls your invite page uses:

authClient.invite.get({ token });
authClient.invite.redeem({ token, ... });

Management

authClient.invite.create({ kind?, type?, email?, name?, role?, maxUses?, expiresIn?,
                           organizationId?, organizationRole?, teamId?, presetSeatLimit? });
authClient.invite.list({ status?, type?, kind?, email?, organizationId?, page?, limit? });
authClient.invite.resend({ inviteId, expiresIn? });
authClient.invite.revoke({ inviteId });
authClient.invite.delete({ inviteId });

resend rotates the token and expiry on any pending invite, invalidating the previous link and redelivering it; see Resending an invitation.

Who may call these is covered in Who can do what.

Mode-specific primitives

Kept for compatibility; invite.redeem routes to the right one for you:

authClient.invite.accept({ token, password, name, email? });
authClient.invite.activate({ token });

accept requires name (it is the flow that populates the profile) and carries the SIGN_UP-step additional fields; activate acts on an existing account, takes no name, and carries the CONFIRM-step additional fields.

Organization administration

authClient.invite.org.usage({ organizationId });
authClient.invite.org.setSeatLimit({ organizationId, seatLimit });
authClient.invite.org.disable({ organizationId, banMembers? });
authClient.invite.org.enable({ organizationId });
authClient.invite.org.delete({ organizationId, banMembers? });

Server-only

These endpoints are never mounted as HTTP routes; they exist only on auth.api.

Create invites headlessly, with no session, for cron jobs and system integrations. Every invariant (email locks, seat limits, role validation) still runs; only the permission gates are skipped. Attribution falls back to your appName, then "System", and createdByUserId is stored as null:

await auth.api.createSystemInvite({
  body: {
    type: "private",
    email: "ada@example.com",
    inviter: { name: "Billing", email: "billing@acme.com" } // optional
  }
});

Trusted server code only

createSystemInvite skips authentication and the admin gate; whoever can call it can mint invites for any role. Keep it in code paths you fully control, never behind a client-reachable route, and never forward unvalidated client input into fields like role, organizationId, maxUses, or inviter. It also thins the audit trail: system invites store a null createdByUserId and a self-declared inviter, so attribution is only as trustworthy as the caller, with the null createdByUserId keeping them distinguishable from human invites.

Preferred inviter name

inviter is the preferred display name for delivery: it flows into inviterName and inviterEmail verbatim, and org-join invites carry organizationName as usual, so "Alex invited you to join Acme" emails render the same as member-sent ones. Without it, the sender shows as your app name or "System".

And for your own scheduler:

const { deleted } = await auth.api.cleanupExpiredInvites();

Rate limits

Built-in limits: /invite/accept, /invite/activate, and /invite/redeem allow 5 requests per 60 seconds; /invite/get allows 10.

Better Auth stores rate-limit counters in memory by default. If you run more than one instance, configure Better Auth's rateLimit.storage (for example "secondary-storage" with Redis) so the limits are shared instead of per-pod. See Operations at scale.

Revoke, delete, and the email lock

In short: invite.revoke cancels an invite but keeps its protections active, invite.delete also removes the pre-created user and frees the address, and accepted invites are permanent audit records that cannot be deleted. The full semantics of the email lock, and why revoking is deliberately softer than deleting, are on the Security page.

Last updated on

On this page