API Endpoints
This document explains the main JSON API flows exposed by Authara. The OpenAPI contract is the exhaustive operation, request, response, and error reference used to generate the SDK clients.
All endpoints are available under:
/auth/api/v1
These endpoints are primarily intended for:
- backend applications
- SDKs
- browser helpers
Authara also provides hosted HTML flows under /auth; applications that own
their authentication UI can use the JSON endpoints documented here.
Authara currently provides no headless admin or operator API. /auth/admin/*
and /auth/operator/* are unstable, server-rendered browser interfaces and
must not be used as automation contracts.
Internal server-to-server endpoints are available under:
/auth/internal/v1
These endpoints are intended for your application backend, not browsers.
When AUTHARA_PUBLIC_ORGANIZATION_MANAGEMENT_ENABLED=true, authenticated
clients can use non-capacity organization-management endpoints under
/auth/api/v1. Organization creation, invitation creation, and invitation
resend remain internal server-to-server operations.
Authentication
Public authentication endpoints do not require a session. Passkey registration and user or organization endpoints require a valid session.
Protected public endpoints accept either the authara_access cookie or an
Authorization: Bearer <access-token> header.
If the access token is missing or invalid, Authara returns:
401 Unauthorized
See Cookies for details.
State-changing requests authenticated through cookies require the
authara_csrf cookie and a matching X-CSRF-Token header. Obtain both by
calling GET /auth/api/v1/csrf first. Public authentication operations made
with session_transport=token, token refresh/revocation, and protected
requests authenticated with a bearer token do not require browser CSRF proof.
Session transports
Authentication operations that declare session_transport accept cookie
(the default) or token. Cookie transport sets session cookies. Token
transport sets no session cookies and returns the access and refresh tokens in
JSON for secure storage by a native client.
For Google, Apple, and Microsoft token flows, call both the provider options
and login endpoints with session_transport=token, and return the options
response's challenge_id with the provider proof. The short-lived challenge
replaces the browser OAuth cookie for state and nonce validation.
Endpoints
Get a CSRF token
GET /auth/api/v1/csrf
Returns 200 OK, sets the authara_csrf cookie, and returns its value:
{
"csrf_token": "<csrf-token>"
}
Send this value as X-CSRF-Token on cookie-transport mutations while
preserving the cookie.
Log in with Google
Google login must be enabled with AUTHARA_OAUTH_PROVIDERS=google.
First initialize the login attempt:
GET /auth/api/v1/oauth/google/options
In the default cookie flow, the response sets the HttpOnly
authara_oauth_nonce cookie. The response is:
{
"challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"client_id": "<google-client-id>",
"nonce": "<nonce>"
}
challenge_id is present for token transport and should be returned in the
login body. It may be omitted in the browser cookie flow.
Initialize a Google popup code client with the returned client ID and the
openid email profile scopes. After Google returns its one-time authorization
code, exchange it with the browser origin:
POST /auth/api/v1/oauth/google?audience=app
X-CSRF-Token: <csrf-token>
Content-Type: application/json
{
"code": "<google-authorization-code>",
"redirect_uri": "https://app.example.com"
}
The native Google flow uses the provider's nonce-bound ID token instead of the browser authorization code:
{
"challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"credential": "<google-id-token>",
"nonce": "<nonce-from-options>"
}
The credential and nonce pair remains supported for browser clients and is
verified against the nonce cookie when challenge_id is omitted.
audience is optional and defaults to app; supported values are app,
admin, and operator. On success, cookie transport sets both session cookies;
token transport returns the user and token pair without setting cookies.
React clients should call both the CSRF and Google-options endpoints with
credentials enabled. Native clients should use session_transport=token on
both calls, return challenge_id, and store the returned refresh token in
platform secure storage.
If the Google email belongs to an existing account that has not linked Google,
the endpoint returns 409 account_link_required with a recovery object. It
contains the pending provider, short-lived link_id, and the existing account
methods that can prove ownership. Continue with the matching recovery endpoint
below; Authara never links accounts based only on a matching email address.
Errors: 400 invalid_request, 401 unauthorized, 403 forbidden,
404 not_found, 409 account_link_required, or 500 internal_error.
Log in with Apple
Apple login must be enabled with AUTHARA_OAUTH_PROVIDERS=apple. First obtain
the Services ID, redirect URI, and one-time state and nonce:
GET /auth/api/v1/oauth/apple/options
Initialize Apple's browser SDK with the returned values, the email scope,
and usePopup: true. Send the resulting single-use code and returned state to:
POST /auth/api/v1/oauth/apple?audience=app
X-CSRF-Token: <csrf-token>
Content-Type: application/json
{
"code": "<apple-authorization-code>",
"state": "<apple-state>"
}
Native clients also include the challenge_id returned by the token-transport
options call.
Authara exchanges the code server-side and validates Apple's signature, issuer, audience, expiry, nonce, subject, and verified email before creating a session. Browser applications must not generate Apple's client-secret JWT or trust identity claims locally.
If the Apple email belongs to an existing account that has not linked that
Apple subject, the endpoint returns 409 account_link_required with the same
typed recovery object. Authara safely stages the Apple refresh token while the
user proves ownership with an existing password, Google, or Apple method.
Errors match Google login: 400 invalid_request, 401 unauthorized,
403 forbidden, 404 not_found, 409 account_link_required, or
500 internal_error.
Log in with Microsoft
Microsoft login must be enabled with AUTHARA_OAUTH_PROVIDERS=microsoft.
Initialize the attempt with:
GET /auth/api/v1/oauth/microsoft/options
The response sets the HttpOnly OAuth nonce cookie and returns the Microsoft
client ID, authority, and nonce. Initialize MSAL Browser with the returned
client ID and authority, request openid profile email through loginPopup,
and use this exact same-origin SPA redirect URI:
https://your-domain/auth/oauth/microsoft/redirect
Core serves the MSAL redirect bridge at that path. After the popup completes, send its ID token and the returned nonce to:
POST /auth/api/v1/oauth/microsoft?audience=app
X-CSRF-Token: <csrf-token>
Content-Type: application/json
{
"credential": "<microsoft-v2-id-token>",
"nonce": "<nonce>"
}
Native clients also include the challenge_id returned by the token-transport
options call.
Core validates the signature, audience, tenant-specific issuer, expiry,
nonce, tenant ID, and object ID. An email collision returns the same
409 account_link_required recovery object as Google and Apple. Microsoft
email claims never merge accounts without proof from an existing sign-in
method, and unverified claims do not mark an Authara email as verified.
Errors match the other external providers.
Apple and Microsoft token transport follows the same pattern as Google: use
session_transport=token on both calls, return the supplied challenge_id
with the provider proof, and do not send browser CSRF or OAuth cookies.
Recover an external-provider account collision
Login collisions for verified external identities return a short-lived pending link directly. A client that needs to start the Google flow explicitly can use a fresh Google authorization code:
POST /auth/api/v1/provider-links/recovery/google
X-CSRF-Token: <csrf-token>
Content-Type: application/json
{
"code": "<google-authorization-code>",
"redirect_uri": "https://app.example.com"
}
The 202 Accepted response identifies the provider being attached and only the
existing account methods that are available as proof. Login collisions already
return this object in their 409 response, so applications normally do not
need to call the start endpoint separately:
{
"link_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"provider": "google",
"proof_methods": ["password", "apple"]
}
Complete exactly one proof through the matching endpoint:
POST /auth/api/v1/provider-links/recovery/{linkID}/password?audience=app
POST /auth/api/v1/provider-links/recovery/{linkID}/google?audience=app
POST /auth/api/v1/provider-links/recovery/{linkID}/apple?audience=app
POST /auth/api/v1/provider-links/recovery/{linkID}/microsoft?audience=app
Password proof accepts { "password": "..." }. Browser Google proof accepts a
fresh code and redirect_uri; native Google proof accepts a fresh
nonce-bound credential with the provider challenge_id. Apple proof accepts
a fresh authorization code and state, and Microsoft proof accepts a fresh
credential and nonce; their native forms also return the provider
challenge_id. Successful completion consumes the pending link, links the
attempted external identity to the existing user, and returns an authenticated
session. Cookie transport sets the session cookies; callers that add
session_transport=token receive the token pair without cookies.
Pending links expire after ten minutes and are single-use. Invalid or expired
links return 400 invalid_request or 409 provider_link_expired; an identity
that does not belong to the existing account returns 401 unauthorized.
Log in with a password
POST /auth/api/v1/login?audience=app
audience is optional and defaults to app; supported values are app,
admin, and operator.
{
"identifier": "[email protected]",
"password": "password123"
}
Migration: clients using the previous password-login schema must rename the
request key from email to identifier; the email value itself is unchanged.
Set AUTHARA_USERNAME_LOGIN_ENABLED=true to also accept the account's username
in the identifier field. The flag defaults to false. When enabled, username
login uses the same request shape:
{
"identifier": "user",
"password": "password123"
}
On success Authara returns 200 OK. Cookie transport sets authara_access and
authara_refresh. A token-transport response is:
{
"user": {
"id": "a8f7c1f5-5d2b-4a3a-91c5-1c87b6e19c41",
"email": "[email protected]",
"email_verified": true,
"username": "user",
"disabled": false,
"created_at": "2026-01-01T12:00:00Z"
},
"access_token": "<access-token>",
"refresh_token": "<refresh-token>"
}
Token transport does not set session cookies or require a CSRF cookie/header pair.
Errors: 400 invalid_request, 401 unauthorized, 403 forbidden,
429 rate_limited, or 500 internal_error.
Sign up directly
POST /auth/api/v1/signup/direct
Signup requires an email address and password and only creates app-audience
sessions. Authara creates the account and returns 201 Created. Cookie
transport sets both session cookies; token transport returns the authentication
response shown above without setting cookies.
This operation returns 404 not_found when challenges are enabled.
Errors: 400 invalid_request, 403 forbidden, 404 not_found, 429
rate_limited, or 500 internal_error.
Start signup verification
POST /auth/api/v1/signup/challenges
The request body is the same email-and-password object used for direct signup.
Authara starts email verification and returns 202 Accepted without creating a
session:
{
"challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba"
}
This operation returns 404 not_found when challenges are disabled.
Errors: 400 invalid_request, 403 forbidden, 404 not_found, 429
rate_limited, or 500 internal_error.
Verify signup verification
POST /auth/api/v1/signup/challenges/verify
{
"challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"code": "123456"
}
On success Authara creates the account and returns 201 Created. Cookie
transport sets both session cookies; token transport returns the authentication
response without setting cookies.
Errors: 400 invalid_request, 403 forbidden, 404 not_found when challenges
are disabled, 429 rate_limited, or 500 internal_error.
Start password reset
POST /auth/api/v1/password-reset/challenges
{
"email": "[email protected]",
"new_password": "new-password123"
}
Returns 202 Accepted with a challenge_id. Password reset rotates an existing
password; it never adds password authentication to a passwordless account. The
response is identical when the email is unknown or the account only uses OAuth
or passkeys, preventing account and sign-in-method enumeration. No reset code is
sent in those cases. A passwordless user must sign in with an existing provider
or passkey and add a password from account settings. If all configured methods
are unavailable, recovery requires the deployment operator's account-recovery
process. This endpoint remains available when optional challenge-based signup
is disabled.
Errors: 400 invalid_request, 403 forbidden, 429 rate_limited, or
500 internal_error.
Verify password reset
POST /auth/api/v1/password-reset/challenges/verify
{
"challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"code": "123456"
}
Returns 204 No Content. The existing password is changed and all existing
sessions for the account are revoked. The user must log in with the new
password. Verification, password mutation, session revocation, notification
queueing, and challenge consumption complete as one database transaction, so a
failed mutation does not consume a valid code. This endpoint remains available
when optional challenge-based signup is disabled.
Errors: 400 invalid_request, 403 forbidden, 429 rate_limited, or
500 internal_error.
Resend a verification code
POST /auth/api/v1/challenges/resend
{
"challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba"
}
Returns 204 No Content. Unknown, expired, consumed, too-recent, and
resend-exhausted challenges also return 204; this intentionally
prevents clients from using resend responses to discover account or challenge
state.
Malformed requests return 400 invalid_request; failed CSRF validation returns
403 forbidden; IP rate limiting returns 429 rate_limited; unexpected
failures return 500 internal_error.
Authenticate with a passkey
Start a WebAuthn authentication ceremony:
POST /auth/api/v1/passkeys/authenticate/options
The request body is empty. Authara returns 200 OK with WebAuthn options:
{
"challenge_id": "7e9cbd7c-a532-4b35-bf1b-3c5237bfb760",
"options": {
"publicKey": {}
}
}
Before calling navigator.credentials.get(), convert the base64url-encoded
challenge and credential IDs in options.publicKey to BufferSource values.
After the browser ceremony, serialize the credential's binary fields back to
base64url strings and submit it:
POST /auth/api/v1/passkeys/authenticate/finish?audience=app
{
"challenge_id": "7e9cbd7c-a532-4b35-bf1b-3c5237bfb760",
"credential": {}
}
audience is optional and defaults to app. On success Authara returns
200 OK. Cookie transport sets both session cookies; token transport returns
the authentication response without setting cookies.
Pass session_transport=token to both the options and finish endpoints for a
native ceremony. The finish response then returns the token pair without
setting cookies or requiring browser CSRF credentials.
Cookie transport requires a valid API CSRF token and may return 403 forbidden.
The options endpoint may also return 429 rate_limited or 500 internal_error.
The finish endpoint may return 400 invalid_request, 401 unauthorized,
429 rate_limited, or 500 internal_error.
Register a passkey
Both registration endpoints require an authenticated app session.
They also require recent authentication and return
428 recent_authentication_required when the session proof is stale.
POST /auth/api/v1/passkeys/register/options
The request body is empty. Authara returns the same challenge_id and options
envelope shown above. Decode its base64url binary fields before passing
options.publicKey to navigator.credentials.create(), then encode the
credential's binary response fields as base64url before submitting it:
POST /auth/api/v1/passkeys/register/finish
{
"challenge_id": "7e9cbd7c-a532-4b35-bf1b-3c5237bfb760",
"credential": {},
"name": "Work laptop",
"platform_hint": "macOS"
}
name and platform_hint are optional. Success returns 204 No Content.
Both endpoints require an access cookie or bearer token. Cookie-authenticated
requests also require CSRF proof. The options endpoint may
return 401 unauthorized, 403 forbidden,
428 recent_authentication_required, or 500 internal_error.
The finish endpoint may return 400 invalid_request, 401 unauthorized,
403 forbidden, 409 passkey_already_exists,
422 passkey_registration_invalid, 428 recent_authentication_required, or
500 internal_error.
Reauthenticate a session
Sensitive account and organization mutations require a fresh proof. An
authenticated client first receives 428 recent_authentication_required from
the attempted mutation. The response contains a five-minute, single-use
challenge bound to the current user and session:
{
"error": {
"code": "recent_authentication_required",
"message": "Recent authentication is required."
},
"authentication_challenge": {
"id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"expires_at": "2026-09-22T12:05:00Z"
},
"reauthenticate_url": "/auth/reauthenticate?authentication_challenge_id=49f7a8b7-5f13-4ab0-9991-e924566a08ba"
}
Complete that challenge through one of these state-changing flows. Cookie authentication requires CSRF proof; bearer authentication does not:
POST /auth/api/v1/reauthenticate/password
POST /auth/api/v1/reauthenticate/google
POST /auth/api/v1/reauthenticate/apple
POST /auth/api/v1/reauthenticate/microsoft
POST /auth/api/v1/reauthenticate/passkeys/options
POST /auth/api/v1/reauthenticate/passkeys/finish
Password reauthentication accepts:
{
"authentication_challenge_id": "49f7a8b7-5f13-4ab0-9991-e924566a08ba",
"password": "..."
}
Google accepts the same authorization-code fields as Google login plus
authentication_challenge_id, and the identity must already be linked to the
current user. Apple accepts its authorization code and state, and Microsoft
accepts its login credential and nonce; both also require the authentication
challenge ID and an identity already linked to the current user. Passkey
options accepts { "authentication_challenge_id": "..." };
passkey finish accepts both that ID and the WebAuthn challenge_id returned by
the options endpoint.
Successful completion returns 204 No Content. The client must then retry the
original sensitive mutation once. Hosted Authara UI flows do this
automatically while keeping the original request only in browser memory. API
clients remain responsible for the retry. Refresh-token use does not extend
the freshness window. Expired, consumed, or session-mismatched challenges
return 409 invalid_authentication_challenge.
Password and passkey proof attempts are rate limited and may return
429 rate_limited.
Get current user
Returns information about the authenticated user.
GET /auth/api/v1/user
Authentication
Required.
Response
200 OK
Example:
{
"id": "a8f7c1f5-5d2b-4a3a-91c5-1c87b6e19c41",
"email": "[email protected]",
"email_verified": true,
"email_verified_at": "2026-01-01T11:30:00Z",
"username": "user",
"roles": [],
"organization": {
"id": "68c673e7-1ff9-4113-8bbf-e00f039a9a61",
"name": "user",
"role": "owner"
},
"disabled": false,
"created_at": "2026-01-01T12:00:00Z"
}
Errors
| Status | Code |
|---|---|
| 401 | unauthorized |
See Errors for error definitions.
Get current account
GET /auth/api/v1/account
Returns the authenticated user, authentication methods, active sessions, and
passkeys. Sessions and passkeys are independently cursor-paginated: use
sessions_limit and sessions_cursor for sessions, and passkeys_limit and
passkeys_cursor for passkeys. Each limit defaults to 50 and must be from 1
through 100. Continue with sessions_next_cursor or passkeys_next_cursor.
Sessions are ordered by creation time and UUID descending; passkeys are ordered
by creation time and UUID ascending. Invalid, cross-user, or cross-collection
cursors return 400 invalid_request.
The hosted /auth/account page applies the same bounds and exposes links for
additional sessions and passkeys.
Compatibility: all pagination query parameters are optional, existing response arrays keep their names and element shapes, and the next-cursor fields are optional. Existing clients therefore receive the first 50 items without a request change and can adopt continuation cursors incrementally.
Set current user password
Creates the authenticated user's password only when the account does not
already have one. The user ID is taken from the access token; clients cannot
supply one. Use PUT /auth/api/v1/account/password to change an existing
password.
PUT /auth/api/v1/users/password
Content-Type: application/json
{
"password": "new-password123"
}
Requires an access cookie or bearer token. Cookie authentication also requires
CSRF proof. Returns 204 No Content and revokes all existing sessions and
refresh tokens. Errors: 400 invalid_request,
401 unauthorized, 403 forbidden, 409 password_already_exists, or
500 internal_error.
Refresh session
Refreshes the current browser cookie session and issues a new access cookie.
POST /auth/api/v1/sessions/refresh
Authentication
Requires the authara_refresh cookie.
Request
The request must include a CSRF token.
Example header:
X-CSRF-Token: <csrf-token>
The token must match the value stored in the authara_csrf cookie.
See Cookies for details.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
audience |
no | Requested token audience; defaults to app |
Example:
POST /auth/api/v1/sessions/refresh?audience=app
Response
200 OK
The response body is empty.
New session cookies are issued:
authara_accessauthara_refresh(depending on rotation policy)
Errors
| Status | Code |
|---|---|
| 401 | unauthorized |
| 400 | invalid_request |
| 500 | internal_error |
See Errors.
Refresh tokens
Refreshes a token session without cookies.
POST /auth/api/v1/tokens/refresh
Request
{
"refresh_token": "<refresh-token>",
"audience": "app"
}
Response
{
"access_token": "<access-token>",
"refresh_token": "<refresh-token>"
}
Errors
| Status | Code |
|---|---|
| 401 | unauthorized |
| 400 | invalid_request |
| 500 | internal_error |
See Errors.
Revoke tokens
Revokes the token-backed session family identified by a JSON refresh token. The operation is idempotent for an unknown or already-revoked token.
POST /auth/api/v1/tokens/revoke
Content-Type: application/json
{
"refresh_token": "<refresh-token>"
}
Success returns 204 No Content. An invalid body returns
400 invalid_request; unexpected failures return 500 internal_error.
Organization Management
Set AUTHARA_PUBLIC_ORGANIZATION_MANAGEMENT_ENABLED=true to allow authenticated
clients to manage non-capacity organization data directly through Authara.
Organization creation, invitation creation, invitation resend, membership
removal, organization deletion, and user deletion remain internal-only so your
application backend can enforce billing, subscriptions, seat limits, and
product-data dependencies.
Public organization routes require an access cookie or bearer access token. State-changing cookie requests also require the API CSRF token. Organization-scoped routes only accept the organization from the current access token. Authara ignores client-supplied actor IDs and applies current membership and owner/admin checks.
Authenticated organization-context routes are always available:
GET /auth/api/v1/capabilities
GET /auth/api/v1/organizations
GET /auth/api/v1/organizations/current
GET /auth/api/v1/organizations/current/members
POST /auth/api/v1/organizations/{organizationID}/switch
Enabling public organization management adds these direct-management routes:
GET /auth/api/v1/organizations/{organizationID}
PATCH /auth/api/v1/organizations/{organizationID}
GET /auth/api/v1/organizations/{organizationID}/members
GET /auth/api/v1/organizations/{organizationID}/members/{userID}
PATCH /auth/api/v1/organizations/{organizationID}/members/{userID}
GET /auth/api/v1/organizations/{organizationID}/invitations
GET /auth/api/v1/organizations/{organizationID}/invitations/{invitationID}
POST /auth/api/v1/organizations/{organizationID}/invitations/{invitationID}/revoke
GET /auth/api/v1/users/{userID}/memberships
The current-user organization, member, membership, and invitation collection
routes use cursor pagination. Pass an optional limit from 1 through 100
(default 50) and the opaque cursor returned as next_cursor by the preceding
response. Results
are ordered deterministically by creation time and UUID. next_cursor is
omitted on the final page. A malformed cursor, a cursor from another
collection or tenant, or an out-of-range limit returns 400 invalid_request.
GET /auth/api/v1/organizations/current/members uses the same pagination
parameters and response behavior.
Hosted administrator user details independently paginate session history and passkeys. The operator email-template editor also bounds version history to 25 entries per page (maximum 100).
The capabilities route reports
allows_public_organization_management: false when direct management is
disabled. The direct-management routes return 404 while the feature is
disabled; organization context, listing, and switching remain available.
In single mode, allows_organization_leave is true because a departure can
be approved by the application backend, while allows_org_switching remains
false: the user may never hold two memberships and cannot switch between
simultaneously available organizations.
Owners and admins can set a non-owner member's role to admin or member.
Ownership is singular and changes only through the ownership-transfer endpoint.
Internal Endpoints
The internal API contains server-to-server lifecycle operations whose product-specific checks belong to the application backend. A webhook cannot veto an operation after it happened, so the application must validate billing and dependencies before calling these endpoints.
Create organization
Creates a team organization on behalf of a user after your application backend has enforced product-specific rules.
POST /auth/internal/v1/organizations
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
Request
{
"name": "Example Inc",
"created_by_user_id": "8d0b28cc-f307-4f0b-8f61-c5c9f736c4b1"
}
Remove an organization member
Removes a membership after the application backend has approved the departure.
Set actor_user_id to the target user for a voluntary leave, or to the owner or
admin performing a removal.
DELETE /auth/internal/v1/organizations/{organization_id}/members/{user_id}
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
{
"actor_user_id": "8d0b28cc-f307-4f0b-8f61-c5c9f736c4b1"
}
In multi mode, non-creators may leave or be removed from a personal
organization; its creator cannot leave. In personal mode, invitations are
disabled, so no additional member can join. Authara also rejects removal of the
last member or sole owner. Sessions currently using the removed organization
and their refresh tokens are deleted; access tokens for that user and
organization are revoked. The operation emits
organization.membership.deleted.
Update an organization member role
PATCH /auth/internal/v1/organizations/{organization_id}/members/{user_id}
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
{
"actor_user_id": "8d0b28cc-f307-4f0b-8f61-c5c9f736c4b1",
"role": "admin"
}
Owners and admins can assign admin or member; admins cannot modify the
owner. Use ownership transfer to change the organization's sole owner.
Transfer organization ownership
Atomically promotes an existing member to owner and demotes the current owner to admin. The actor must be an owner, the new owner must already belong to the team organization, and both users are protected from concurrent deletion while the transfer runs.
POST /auth/internal/v1/organizations/{organization_id}/ownership-transfer
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
{
"actor_user_id": "8d0b28cc-f307-4f0b-8f61-c5c9f736c4b1",
"new_owner_user_id": "67a1123a-e04f-44c2-aae9-314e28dcbd9c"
}
Personal organization ownership cannot be transferred. A successful transfer
emits organization.membership.updated for both memberships and allows the
former owner to leave through the member-removal endpoint.
Delete an organization
Permanently deletes a team organization after application-owned subscription and data checks have passed. Only an owner may perform this operation.
DELETE /auth/internal/v1/organizations/{organization_id}
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
{
"actor_user_id": "8d0b28cc-f307-4f0b-8f61-c5c9f736c4b1"
}
Personal organizations cannot be deleted through this endpoint. In single
mode, organization deletion is accepted only when the actor is the sole member;
an organization with other members requires a separately designed tenant
teardown. In multi mode, all memberships may be removed because users retain
their personal organizations. A successful deletion removes organization-bound
sessions and invitations and emits organization.deleted.
Delete a user
Permanently deletes a user after account-level application checks have passed.
DELETE /auth/internal/v1/users/{user_id}
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
User deletion cannot bypass organization invariants: Authara rejects deletion
when the user is the last member or sole owner of a team organization. An owned
personal organization is deleted as part of the user deletion; in multi mode,
memberships in other users' personal organizations are removed. Users can also
delete their account from Authara's hosted account page. Authara emits the
user.deleted webhook so applications can clean up product data and billing.
Create organization invitation
Creates a secure Authara organization invitation and returns the invitation link.
Your application backend should call this after it has enforced product-specific rules such as billing and seat limits. actor_user_id is required; Authara validates that the actor is a member of the organization and is allowed to invite members. role controls the Authara organization role granted on acceptance and may be member or admin; it defaults to member. The owner role remains reserved for the organization creator. metadata is optional opaque application JSON that Authara stores and forwards unchanged.
POST /auth/internal/v1/organizations/{organization_id}/invitations
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
Request
{
"actor_user_id": "8d0b28cc-f307-4f0b-8f61-c5c9f736c4b1",
"email": "[email protected]",
"role": "admin",
"metadata": {
"baufunk": {
"role": "manager"
}
}
}
Response
{
"invitation": {
"id": "7ea9ce22-72bb-45bd-96d2-7368314dd345",
"organization_id": "68c673e7-1ff9-4113-8bbf-e00f039a9a61",
"email": "[email protected]",
"role": "admin",
"metadata": {
"baufunk": {
"role": "manager"
}
},
"status": "pending",
"expires_at": "2026-01-08T12:00:00Z",
"invite_url": "https://example.com/auth/invitations/accept?token=..."
}
}
Authara also enqueues an invitation email when the email worker is configured. The returned invite_url is always present for testing or app-owned delivery.
In single mode, an existing account may accept an invitation only after its
previous membership has been removed. The hosted invitation page keeps the
invitation pending and tells the user to leave or delete the current organization
through the application. After the application backend approves that operation
and calls the internal API, the user can reopen the invitation, log in, and
accept it normally.
Resend organization invitation
Replaces an existing invitation with a fresh pending invitation.
POST /auth/internal/v1/organizations/{organization_id}/invitations/{invitation_id}/resend
Authorization: Bearer <AUTHARA_INTERNAL_API_TOKEN>
Errors
| Status | Code |
|---|---|
| 401 | unauthorized |
| 403 | actor_not_member |
| 403 | actor_not_allowed |
| 404 | organization_not_found |
| 409 | already_member |
| 409 | invitation_already_pending |
| 400 | invalid_request |
| 500 | internal_error |
Versioning
Authara uses path-based versioning.
Current version:
/auth/api/v1
Future versions may introduce new endpoints under /auth/api/v2.
Summary
Authara exposes a versioned API for:
- authentication, provider linking, passkeys, and account recovery
- account, credential, and session management
- user and organization lifecycle operations
- cookie and native token transports
Additional endpoints may be introduced in future versions.