Organizations
Multi-tenant orgs — creating and managing them, membership and roles, active-org scoping, and invites. curl, Go, and a browser client.
Organizations
Multi-tenancy: users can belong to more than one organization, hold a different role in each, and scope a session to one of them at a time. Disabled by default — with WithOrganizations unset (or Enable: false), none of the routes on this page are mounted at all, and h.services.Org is nil.
For state-changing requests, every curl example below needs the same -H "Origin: https://myapp.com" treatment as Authentication — omitted from most snippets below for brevity, but required in practice.
Configuration
goauth.WithOrganizations(goauth.OrganizationConfig{
Enable: true, // optional, default false
MaxOrgsPerUser: 10, // optional, default 0 (built-in cap of 100)
InviteTTL: 7 * 24 * time.Hour, // optional, default 7d
})| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
Enable | bool | Optional | false | Nothing on this page exists until this is true. |
MaxOrgsPerUser | int | Optional | 0 — built-in cap of 100 | Counts orgs a user owns, not every org they're a member of. Enforced on both org creation and being granted/invited as Owner. |
InviteTTL | time.Duration | Optional | 7d | How long a code from Creating an invite stays valid. |
Expected errors
organizations.max_orgs_per_user must be between 0 and 100 (0 = default 100)organizations.invite_ttl must be positive— hint: only reachable via an explicit negative value.
Full reference: Configuration → WithOrganizations.
Frontend client setup
The Client examples below call the same apiRequest helper defined on Client → Setup.
Roles
Roles are compared by weight, not by name. RequireOrgRole(min) checks role.Weight() >= min.Weight(), so “Admin or above” means weight 2 or higher.
| Role | Weight | Can |
|---|---|---|
owner | 3 | Everything, including deleting the org and granting/revoking Owner |
admin | 2 | Manage members and invites, update org name/slug — everything except delete the org or touch Owner status |
member | 1 | Read the org, leave it |
Granting or revoking Owner requires being an Owner yourself
This is checked separately from the route-level RequireOrgRole, inside the service layer: Change a member's role and Creating an invite both special-case the owner role — an Admin can promote a Member to Admin (that's normal RequireOrgRole(admin) territory), but cannot self-escalate to Owner or hand Owner to anyone else. Only an existing Owner can create or remove another Owner. Attempting it as anything less returns org_forbidden, distinct from the route-level 403 you'd get for not being an Admin at all.
Every org-scoped route below runs through RequireOrgMember first (reads {orgID} from the path, 404 org_member_not_found if you're not a member — indistinguishable from the org not existing, on purpose), then RequireOrgRole if the action needs more than plain membership.
Creating and reading orgs
Create an org — POST /auth/orgs
Auth: any authenticated user. Body: { "name", "slug" }. The caller becomes the org's first member, with role owner.
{
"id": "a1b2...",
"name": "Acme Inc",
"slug": "acme",
"createdBy": "b3f1...",
"ownerCount": 1,
"memberCount": 1,
"metadata": {},
"createdAt": "2026-08-09T12:00:00Z",
"updatedAt": "2026-08-09T12:00:00Z"
}| Code | Status | Cause |
|---|---|---|
invalid_name | 400 | name is empty |
invalid_slug | 400 | slug is over 255 bytes |
org_slug_reserved | 400 | slug is one of the reserved words (api, admin, auth, www, ... — see Configuration for the full list) |
org_slug_exists | 409 | Another org already has that slug |
org_limit_reached | 400 | Caller already owns MaxOrgsPerUser orgs |
curl
curl -X POST https://api.myapp.com/auth/orgs \
-H "Content-Type: application/json" \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txt \
-d '{"name":"Acme Inc","slug":"acme"}'Programmatic (Go)
org, err := auth.Services.Org.CreateOrg(ctx, goauth.CreateOrgInput{
Name: "Acme Inc",
Slug: "acme",
OwnerID: userID,
})Client
See Create organization for the wrapper and form component.
List your orgs — GET /auth/orgs
Auth: any authenticated user. Every org the caller belongs to, any role. Query params, all optional:
| Param | Values | Default | Notes |
|---|---|---|---|
role | owner, admin, member | — (no filter) | Narrows to memberships at that role — "the orgs I own". Anything else is a 400, not a silent fallback; see the callout below. |
search | any string | — (no filter) | Substring match on org name. |
orderBy | name, created_at, member_count | name | Unrecognized values silently fall back to the default rather than erroring. |
orderDirection | asc, desc | asc | Same fallback behavior. |
offset | integer | 0 | |
limit | integer | 20 | Omit entirely for the default page of 20. limit=0 explicitly means unlimited — every org the caller belongs to, in one response. Any other value is capped at 100. This distinction only exists because the param was left off vs. sent as 0 — see the callout below. |
An unrecognized role is a 400 — an unrecognized orderBy isn't
The two look like the same kind of param and are deliberately handled differently, here and on every org listing on this page (members, invites, and the admin equivalents).
A bad orderBy falls back to the default because a wrong sort order is cosmetic and immediately visible on screen. A bad role is rejected because a filter that silently widens returns rows the caller explicitly asked to exclude — ?role=Owner (capital O, straight out of a dropdown label) would hand back every member, and a console rendering them under the heading it filtered by would be confidently wrong with no error anywhere. An empty role= still means "no filter", so a <select> bound directly to the query param works unchanged.
The service layer already treats a role this way on every mutation input; as of v0.2.2 the filters agree with it.
Why limit=0 means something different from omitting it
ListUserOrgsInput.Limit is *int, not int, specifically so the service can tell "the caller didn't say" (nil → default 20) apart from "the caller explicitly wants everything" (&0 → unlimited). A plain int can't make that distinction — Go's zero value is 0 either way. The HTTP handler mirrors this by checking whether the limit query param is present, not just parsing it: an absent or unparsable limit becomes nil, limit=0 becomes a pointer to 0. The same semantics apply to members and invites below — but not to the admin GET /admin/users listing, which has no unlimited escape hatch.
{
"orgs": [
{
"id": "b3b12...",
"name": "Acme Inc",
"slug": "acme",
"memberCount": 5,
"createdAt": "2026-01-15T10:30:00Z"
}
],
"limit": 20,
"offset": 0
}The list response carries no total. Fetch it from GET /auth/orgs/count — the count of orgs matching search (ignoring limit/offset), which is what you paginate against, not orgs.length. Splitting it out keeps List from running a COUNT(*) on every page.
curl
curl "https://api.myapp.com/auth/orgs?search=acme&orderBy=created_at&orderDirection=desc&limit=20&offset=0" \
-b cookies.txt -c cookies.txt
curl "https://api.myapp.com/auth/orgs/count?search=acme" -b cookies.txt -c cookies.txt
# → { "count": 1 }Programmatic (Go)
result, err := auth.Services.Org.ListUserOrgs(ctx, goauth.ListUserOrgsInput{
UserID: userID, Offset: 0, Limit: nil, // nil = default 20; &0 = unlimited
OrderBy: "created_at", OrderDirection: "desc",
})
// result.Orgs, result.Limit, result.Offset
n, err := auth.Services.Org.CountUserOrgs(ctx, goauth.ListUserOrgsInput{
UserID: userID, Search: &search,
})Role is a *domain.OrgRole — leave it nil for every role, or point it at one to narrow the list to memberships at that role:
role := domain.OrgRoleOwner
owned, err := auth.Services.Org.ListUserOrgs(ctx, goauth.ListUserOrgsInput{
UserID: userID, Role: &role,
})Client
See List organizations for the wrapper and data table.
Get one org — GET /auth/orgs/{orgID}
Auth: org member. Same response shape as create.
| Code | Status | Cause |
|---|---|---|
org_not_found | 404 | Doesn't exist |
org_member_not_found | 404 | Exists, but the caller isn't a member — same status either way, so membership can't be probed |
curl
curl https://api.myapp.com/auth/orgs/a1b2... -b cookies.txt -c cookies.txtProgrammatic (Go)
org, err := auth.Services.Org.GetByID(ctx, goauth.GetOrgInput{OrgID: orgID, ActorID: callerUserID})Client
See Get organization for the wrapper.
Updating and deleting orgs
Update an org — PUT /auth/orgs/{orgID}
Auth: org admin or above. Body: { "name"?, "slug"? }. Both fields are pointers, so omitting one leaves it unchanged rather than clearing it. Unlike WithRegistration, this is a partial update.
```json
{
"id": "b3b12...",
"name": "Acme Incorporated",
"slug": "acme",
"memberCount": 5,
"createdAt": "2026-01-15T10:30:00Z"
}
| Code | Status | Cause |
|---|---|---|
| `org_not_found` | 404 | Doesn't exist |
| `org_slug_reserved` | 400 | New `slug` is a reserved word |
| `org_slug_exists` | 409 | New `slug` is already taken by another org |
---
#### curl
```bash
curl -X PUT https://api.myapp.com/auth/orgs/a1b2... \
-H "Content-Type: application/json" \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txt \
-d '{"name":"Acme Incorporated"}'Programmatic (Go)
newName := "Acme Incorporated"
org, err := auth.Services.Org.UpdateOrg(ctx, goauth.UpdateOrgInput{
OrgID: orgID,
Name: &newName, // nil Slug leaves the slug unchanged
})Client
See Update organization for the wrapper and form component.
Delete an org — DELETE /auth/orgs/{orgID}
Auth: org owner. Deletes the org outright — no soft-delete. Every member's activeOrgId pointing at it is cleared first (their sessions survive; they're just no longer scoped to a now-gone org).
{ "message": "Organization deleted" }The only error beyond the standard auth ones is org_not_found (404).
curl
curl -X DELETE https://api.myapp.com/auth/orgs/a1b2... \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txtProgrammatic (Go)
err := auth.Services.Org.DeleteOrg(ctx, goauth.DeleteOrgInput{OrgID: orgID, ActorID: callerUserID})Client
See Delete organization for the wrapper and confirmation dialog.
Members
List members — GET /auth/orgs/{orgID}/members
Auth: org member. Query params, all optional:
| Param | Values | Default | Notes |
|---|---|---|---|
role | owner, admin, member | — (no filter) | Anything else is a 400 — see the callout under List your orgs. |
search | any string | — (no filter) | Substring match on the member's name or email. |
orderBy | joined_at, role, name, email | joined_at | Unrecognized values fall back to the default. |
orderDirection | asc, desc | asc | Same fallback behavior. |
offset | integer | 0 | |
limit | integer | 20 | Omit for the default page of 20. limit=0 is explicit unlimited — every member of the org, in one response. Other values are capped at 100. See the limit=0 callout under List your orgs — same semantics here. |
{
"members": [
{
"orgId": "a1b2...",
"userId": "b3f1...",
"role": "owner",
"joinedAt": "2026-08-01T09:00:00Z",
"user": { "...": "the full domain.User for this member" }
}
],
"limit": 20,
"offset": 0
}The total lives at GET /auth/orgs/{orgID}/members/count (accepts role and search, ignores pagination) so the list doesn't run a COUNT(*) per page.
curl
curl "https://api.myapp.com/auth/orgs/a1b2.../members?role=admin&search=jane&orderBy=name&orderDirection=asc&offset=0&limit=20" \
-b cookies.txt -c cookies.txt
curl "https://api.myapp.com/auth/orgs/a1b2.../members/count?role=admin&search=jane" \
-b cookies.txt -c cookies.txt
# → { "count": 1 }Programmatic (Go)
result, err := auth.Services.Org.ListMembers(ctx, goauth.ListMembersInput{
OrgID: orgID, ActorID: callerUserID, Offset: 0, Limit: nil, // nil = default 20; &0 = unlimited
OrderBy: "name", OrderDirection: "asc",
})
// result.Members, result.Limit, result.Offset
n, err := auth.Services.Org.CountMembers(ctx, goauth.ListMembersInput{
OrgID: orgID, ActorID: callerUserID, Role: &role, Search: &search,
})Client
See List members for the wrapper and data table.
Remove a member, or leave — DELETE /auth/orgs/{orgID}/members/{userID}, POST /auth/orgs/{orgID}/leave
Two routes, one underlying operation: LeaveOrg is literally RemoveMember with your own ID — leaving is just removing yourself. Removing someone else requires org admin; leaving only requires being a member (you can always remove yourself).
{ "message": "Member removed" }or, from /leave:
{ "message": "Left organization" }| Code | Status | Cause |
|---|---|---|
org_member_not_found | 404 | That user isn't a member |
cannot_remove_last_owner | 400 | Target is the org's only Owner — removing (or leaving as) the last Owner is blocked so an org can never end up with zero owners |
curl
# Remove someone else (admin+)
curl -X DELETE https://api.myapp.com/auth/orgs/a1b2.../members/b3f1... \
-H "Origin: https://myapp.com" -b cookies.txt -c cookies.txt
# Leave it yourself
curl -X POST https://api.myapp.com/auth/orgs/a1b2.../leave \
-H "Origin: https://myapp.com" -b cookies.txt -c cookies.txtProgrammatic (Go)
err := auth.Services.Org.RemoveMember(ctx, goauth.RemoveMemberInput{
OrgID: orgID, UserID: targetUserID, ActorID: callerUserID,
})
// leaving is the same call with your own ID as UserID (the actor is you):
err = auth.Services.Org.LeaveOrg(ctx, goauth.LeaveOrgInput{OrgID: orgID, UserID: callerUserID})Client
See Remove member and Leave organization for the wrapper and confirmation dialogs.
Change a member's role — PATCH /auth/orgs/{orgID}/members/{userID}/role
Auth: org admin — except granting or revoking owner, which requires the caller to already be an Owner (see the Roles callout above). Body: { "role" }.
{ "message": "Role updated" }| Code | Status | Cause |
|---|---|---|
invalid_role | 400 | role isn't owner/admin/member |
org_member_not_found | 404 | Target isn't a member |
org_forbidden | 403 | Caller is an Admin (not Owner) trying to grant or revoke owner |
cannot_remove_last_owner | 400 | Target is the sole Owner and the new role isn't owner |
curl
curl -X PATCH https://api.myapp.com/auth/orgs/a1b2.../members/b3f1.../role \
-H "Content-Type: application/json" \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txt \
-d '{"role":"admin"}'Programmatic (Go)
err := auth.Services.Org.UpdateMemberRole(ctx, goauth.UpdateMemberRoleInput{
OrgID: orgID,
UserID: targetUserID,
NewRole: domain.OrgRoleAdmin,
ActorID: callerUserID, // whose own membership gets checked for the Owner-only rule above
})Client
See Change member role for the wrapper and role select component.
Active org
A session can be scoped to one org at a time — stored as sessions.active_org_id/active_org_role (see Schemas → sessions), returned on every session-listing response from Sessions. Your app reads it to decide which org's data the current request should see.
Tenant-safe application handlers
For routes that query tenant-owned application data, use RequireActiveOrgScope. It authenticates the request, checks that the session has an active organization with at least the required role, and invokes the handler with a non-empty goauth.OrgScope only after those checks succeed:
type ProjectStore struct {
db *sql.DB
}
func (s *ProjectStore) Get(ctx context.Context, scope goauth.OrgScope, projectID string) (*Project, error) {
const query = `
SELECT id, name
FROM projects
WHERE org_id = $1 AND id = $2`
var project Project
err := s.db.QueryRowContext(ctx, query, scope.OrgID, projectID).
Scan(&project.ID, &project.Name)
return &project, err
}
func getProject(w http.ResponseWriter, r *http.Request, scope goauth.OrgScope) {
project, err := projects.Get(r.Context(), scope, r.PathValue("projectID"))
// handle err and encode project
}
mux.Handle(
"GET /projects/{projectID}",
auth.CORS(auth.RequireActiveOrgScope(domain.OrgRoleMember, getProject)),
)RequireActiveOrgScope includes RequireAuth; do not wrap it a second time. It returns no_active_org (400) if the session has no active org and org_forbidden (403) if its role is below the required role. For a route that names the tenant in an {orgID} path segment, use RequireOrgScope with the same handler signature instead.
Org authorization does not rewrite your SQL
OrgScope makes the tenant binding an explicit handler and repository argument, but go-auth cannot inject org_id into queries issued by your application. Every read, update, and delete of tenant-owned data must include scope.OrgID in its predicate. Filtering only by a caller-controlled resource ID can expose another tenant's row, even when this middleware authorized the request itself.
The lower-level RequireActiveOrg(role) and RequireOrg(role) middleware remain available when a conventional http.Handler is required. Wrap them inside RequireAuth, then read the resolved tenant with middleware.GetOrgID(ctx) and middleware.GetOrgRole(ctx). They perform the same authentication and role checks, but recovering the org from context is easier to forget than accepting OrgScope directly, so prefer the scoped variants for tenant-owned data routes.
An explicit {orgID} and the session's active organization are independent selections and can legitimately differ. RequireOrgScope authorizes the path organization; RequireActiveOrgScope authorizes the active organization stored on the session. Choose one source for a route and use the resulting scope.OrgID all the way through its repository calls.
From a browser client, there's no dedicated "what's my active org" endpoint — /auth/me returns the user, not the session. The active org is visible on the session-listing responses (match currentSessionId against the sessions array to find your own), but the simplest approach is usually to just track it as local state the moment you call setActive/clearActive below, rather than re-fetching to confirm it.
Set active org — PUT /auth/orgs/active
Auth: any authenticated user. Body: { "orgId" }. No {orgID} path segment — membership is checked inside the service, not by RequireOrgMember middleware.
{ "message": "Active org updated" }The only error beyond the standard auth ones is org_member_not_found (404) — you can't activate an org you don't belong to.
curl
curl -X PUT https://api.myapp.com/auth/orgs/active \
-H "Content-Type: application/json" \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txt \
-d '{"orgId":"a1b2..."}'Programmatic (Go)
err := auth.Services.Org.SetActiveOrg(ctx, goauth.SetActiveOrgInput{SessionID: sessionID, UserID: userID, OrgID: orgID})Client
See Set active organization for the wrapper and org switcher component.
Clear active org — DELETE /auth/orgs/active
Auth: any authenticated user. No body.
{ "message": "Active org cleared" }curl
curl -X DELETE https://api.myapp.com/auth/orgs/active \
-H "Origin: https://myapp.com" -b cookies.txt -c cookies.txtProgrammatic (Go)
err := auth.Services.Org.ClearActiveOrg(ctx, goauth.ClearActiveOrgInput{SessionID: sessionID})Client
The client has no dedicated clear-active wrapper — call the endpoint directly (see Setup).
Invites
Email-based, code-authenticated, and matched by email on accept — the accepting account's email must equal the invite's, so an invite can't be redeemed by whoever happens to have the link.
Creating an invite — POST /auth/orgs/{orgID}/invites
Auth: org admin — except inviting someone directly as owner, which requires the caller to already be an Owner, mirroring Change a member's role. Body: { "email", "role" }. Requires a mailer (WithMailer/WithEmail) — same requirement as email verification; without one, this fails outright rather than silently skipping delivery.
{
"id": "c4d5...",
"orgId": "a1b2...",
"email": "new-hire@example.com",
"role": "member",
"rawCode": "the-raw-code",
"invitedBy": "b3f1...",
"expiresAt": "2026-08-16T12:00:00Z",
"createdAt": "2026-08-09T12:00:00Z"
}rawCode only appears here, on creation — List invites omits it, since by then it's only useful to whoever received the email.
| Code | Status | Cause |
|---|---|---|
invalid_role | 400 | role isn't owner/admin/member |
org_forbidden | 403 | Caller is an Admin (not Owner) inviting as owner |
email_failed | 500 | Invite row was created, but sending the email failed |
internal_error | 500 | InviteTTL isn't configured (WithOrganizations wasn't set up with a positive InviteTTL) |
curl
curl -X POST https://api.myapp.com/auth/orgs/a1b2.../invites \
-H "Content-Type: application/json" \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txt \
-d '{"email":"new-hire@example.com","role":"member"}'Programmatic (Go)
invite, err := auth.Services.OrgInvite.CreateOrgInvite(ctx, goauth.CreateOrgInviteInput{
OrgID: orgID,
Email: "new-hire@example.com",
Role: domain.OrgRoleMember,
InvitedBy: callerUserID,
})Client
See Create invite for the wrapper and invite form component.
Accepting an invite — POST /auth/orgs/invites/accept
Auth: any authenticated user — the invited person has to already have (or just have created) an account; this only adds them to the org. Body: { "code" }. No {orgID} segment — the org comes from the code itself.
{ "message": "Invite accepted" }| Code | Status | Cause |
|---|---|---|
invalid_code | 400 | code was empty |
org_invite_expired | 400 | Code doesn't match any invite, or matches one that's already been claimed or has expired — all three collapse to this one code |
org_invite_email_mismatch | 400 | The authenticated caller's email doesn't match the invite's — log in (or sign up) as the invited address first |
curl
curl -X POST https://api.myapp.com/auth/orgs/invites/accept \
-H "Content-Type: application/json" \
-H "Origin: https://myapp.com" \
-b cookies.txt -c cookies.txt \
-d '{"code":"the-raw-code"}'Programmatic (Go)
err := auth.Services.OrgInvite.AcceptInvite(ctx, goauth.AcceptInviteInput{
UserID: callerUserID,
RawCode: code,
})Client
See Accept invite for the wrapper and accept form component.
Listing invites — GET /auth/orgs/{orgID}/invites
Auth: org admin. rawCode is never present here (see Creating an invite). Query params, all optional:
| Param | Values | Default | Notes |
|---|---|---|---|
role | owner, admin, member | — (no filter) | The role the invite grants on acceptance, not the caller's own role. Anything else is a 400, as on every org listing. |
status | pending, expired | — (both) | Derived from expiresAt vs. the current time — there's no stored status column. A claimed invite's row is deleted outright, so it never shows up here as anything. |
search | any string | — (no filter) | Substring match on the invited email. |
orderBy | created_at, expires_at, email, role | created_at | Unrecognized values fall back to the default. |
orderDirection | asc, desc | desc | Note the default direction here is desc (newest first), unlike orgs/members above — matches the previous fixed ordering. |
offset | integer | 0 | |
limit | integer | 20 | Omit for the default page of 20. limit=0 is explicit unlimited — every invite for the org, in one response. Other values are capped at 100. See the limit=0 callout under List your orgs — same semantics here. |
{
"invites": [
{
"id": "inv_abc...",
"email": "new-hire@example.com",
"role": "member",
"status": "pending",
"expiresAt": "2026-01-22T10:30:00Z",
"createdAt": "2026-01-15T10:30:00Z"
}
],
"limit": 20,
"offset": 0
}The total lives at GET /auth/orgs/{orgID}/invites/count (accepts role, status, search; ignores pagination) so the list doesn't run a COUNT(*) per page.
curl
curl "https://api.myapp.com/auth/orgs/a1b2.../invites?status=pending&search=new-hire&orderBy=created_at&orderDirection=desc&offset=0&limit=20" \
-b cookies.txt -c cookies.txt
curl "https://api.myapp.com/auth/orgs/a1b2.../invites/count?status=pending&search=new-hire" \
-b cookies.txt -c cookies.txt
# → { "count": 1 }Programmatic (Go)
result, err := auth.Services.OrgInvite.ListOrgInvites(ctx, goauth.ListOrgInvitesInput{
OrgID: orgID, ActorID: callerUserID, Offset: 0, Limit: nil, // nil = default 20; &0 = unlimited
Status: statusPtr("pending"), OrderBy: "created_at", OrderDirection: "desc",
})
// result.Invites, result.Limit, result.Offset
n, err := auth.Services.OrgInvite.CountOrgInvites(ctx, goauth.ListOrgInvitesInput{
OrgID: orgID, ActorID: callerUserID, Status: statusPtr("pending"),
})Client
See List invites for the wrapper and data table.
Resending or deleting an invite — POST .../invites/{inviteID}/resend, DELETE .../invites/{inviteID}
Auth: org admin for both. orgID in the path is only used for the membership/role check — the handler looks up the invite by inviteID alone, so it isn't re-validated against orgID a second time inside the handler itself.
{ "message": "Invite email resent" }or:
{ "message": "Invite deleted" }Delete here is a hard delete of the invite record — distinct from revoking a self-service signup invite (see Routes → Invites), which is a different feature with its own soft-cancel semantics.
curl
curl -X POST https://api.myapp.com/auth/orgs/a1b2.../invites/c4d5.../resend \
-H "Origin: https://myapp.com" -b cookies.txt -c cookies.txt
curl -X DELETE https://api.myapp.com/auth/orgs/a1b2.../invites/c4d5... \
-H "Origin: https://myapp.com" -b cookies.txt -c cookies.txtProgrammatic (Go)
err := auth.Services.OrgInvite.ResendOrgInviteEmail(ctx, inviteID)
err = auth.Services.OrgInvite.DeleteOrgInvite(ctx, inviteID)Client
See Resend invite and Delete invite for the wrapper and confirmation dialogs.
Next
- Routes — the full path/param table for every route on this page
- Schemas — the
organizations,organization_members, andorganization_invitestables - Authentication — the mailer requirement invites share with email verification
- Sessions — where
activeOrgId/activeOrgRoleshow up on a session
Sessions
Checking who's logged in, listing active sessions (paginated and not), and revoking them — one, several, or all. curl, Go, and a browser client.
Security
SecurityConfig — origin allow-list, CSRF, and password policy — plus every self-service account action: name, password, verification, and deletion.