All endpoints are prefixed with your Better Auth base path (default: /api/auth).
Create a new invitation. Requires admin session.
Body:
{
"email": "user@example.com",
"sendEmail": true,
"maxUses": 1,
"metadata": { "team": "engineering", "role": "member" }
}| Field | Type | Default | Description |
|---|---|---|---|
| string | required | Invitee email address | |
| sendEmail | boolean | true |
Whether to send the invitation email |
| maxUses | number | 1 |
Maximum number of times this code can be used (1-10,000) |
| metadata | object | null |
Arbitrary metadata to attach to the invitation |
Response:
{
"id": "clx...",
"code": "a1b2c3d4e5f6...",
"email": "user@example.com",
"inviteUrl": "https://app.com/register?invite=a1b2c3d4e5f6...",
"expiresAt": "2026-02-27T18:00:00.000Z",
"emailSent": true,
"maxUses": 1,
"metadata": { "team": "engineering", "role": "member" }
}The plaintext code is returned exactly once. It is hashed before storage and cannot be retrieved again.
Create multiple invitations at once. Requires admin session. Max 50 per call.
Body:
{
"invitations": [
{ "email": "alice@example.com", "sendEmail": true },
{ "email": "bob@example.com", "sendEmail": true, "maxUses": 5 },
{ "email": "carol@example.com", "sendEmail": false, "metadata": { "dept": "sales" } }
]
}Response:
{
"items": [
{ "id": "...", "code": "...", "email": "alice@example.com", "inviteUrl": "...", "expiresAt": "...", "emailSent": true, "maxUses": 1, "metadata": null },
{ "id": "...", "code": "...", "email": "bob@example.com", "inviteUrl": "...", "expiresAt": "...", "emailSent": true, "maxUses": 5, "metadata": null },
{ "id": "...", "code": "...", "email": "carol@example.com", "inviteUrl": "...", "expiresAt": "...", "emailSent": false, "maxUses": 1, "metadata": { "dept": "sales" } }
],
"count": 3
}List invitations with optional filters. Requires admin session.
Query params:
status--"all"|"pending"|"used"|"expired"|"revoked"(default:"all")limit-- 1-100 (default: 50)cursor-- ISO date string for cursor pagination
Response:
{
"items": [
{
"id": "...",
"email": "...",
"invitedBy": "...",
"maxUses": 1,
"useCount": 0,
"status": "pending",
"expiresAt": "...",
"createdAt": "...",
"metadata": { "team": "engineering" }
}
],
"nextCursor": "2026-02-19T12:00:00.000Z"
}Revoke an invitation (soft-delete). Requires admin session.
Body: { "id": "invitation-id" }
Response: { "success": true }
Resend the invitation email. Requires admin session. This revokes the old invitation and creates a new one with a fresh code -- the original hashed code cannot be recovered, so a clean replacement is the only option.
Fails if sendInviteEmail is not configured.
Body: { "id": "invitation-id" }
Response:
{ "success": true, "newInvitationId": "...", "inviteUrl": "..." }Permanently delete an invitation record. Requires admin session. Use for GDPR compliance or cleanup.
Body: { "id": "invitation-id" }
Response: { "success": true }
Check if an invite code is valid. Public endpoint. Does not return email or any PII.
Body: { "code": "a1b2c3d4..." }
Response:
{ "valid": true, "expiresAt": "2026-02-27T..." }Aggregate invitation statistics. Requires admin session.
Response:
{ "total": 42, "pending": 15, "used": 20, "expired": 5, "revoked": 2 }Check if invite-only mode is active. Public endpoint. Useful for conditionally rendering the invite code field on your registration page.
Response:
{ "enabled": true }Every error the plugin throws is a named, typed error code. Import them if you need to handle specific failures -- or don't, and let the message strings do the talking.
import { ERROR_CODES } from "better-auth-invitation-only";
// Each code is { code: string, message: string }
ERROR_CODES.INVITE_REQUIRED.code; // "INVITE_REQUIRED"
ERROR_CODES.INVITE_REQUIRED.message; // "Invitation code required"| Code | HTTP Status | Message |
|---|---|---|
INVITE_REQUIRED |
403 | Invitation code required |
INVALID_INVITE |
403 | Invalid or expired invitation code |
INVITE_EXPIRED |
403 | Invitation code expired |
INVITE_EXHAUSTED |
403 | Invitation has reached maximum uses |
EMAIL_MISMATCH |
403 | This invitation code is for a different email address |
ADMIN_REQUIRED |
403 | Admin access required |
NOT_FOUND |
404 | Invitation not found |
ALREADY_USED |
400 | Cannot revoke a used invitation |
ALREADY_REVOKED |
400 | Invitation already revoked |
NO_LONGER_VALID |
400 | Invitation is no longer valid |
DOMAIN_NOT_ALLOWED |
400 | Email domain is not allowed |
BATCH_EMPTY |
400 | At least one invitation is required |
EMAIL_NOT_CONFIGURED |
400 | Email sending not configured |
EMAIL_SEND_FAILED |
500 | Failed to send email |
TOO_MANY_PENDING |
429 | Too many pending signups |
Error responses follow the Better Auth convention:
{
"code": "INVITE_REQUIRED",
"message": "Invitation code required",
"status": 403
}Default rate limits (configurable via the rateLimits option):
| Endpoint | Max | Window |
|---|---|---|
/invite-only/validate |
10 | 60s |
/invite-only/create |
20 | 60s |
/invite-only/create-batch |
20 | 60s |
/invite-only/resend |
10 | 60s |
Override defaults:
inviteOnly({
rateLimits: {
validate: { max: 5, window: 30 },
create: { max: 50, window: 120 },
resend: { max: 5, window: 60 },
},
})