Skip to content

Authentication

The API exposes two authentication surfaces.

Public clients use the stable /api/v1/* JWT endpoints documented on this page. Browser-based and server-rendered flows can also use Better Auth session endpoints mounted at /api/auth/*.

Phone OTP is supported only through the versioned /api/v1/auth/phone/* routes on this page. The underlying Better Auth phone endpoints are not exposed as part of the public /api/auth/* surface.

Protected routes that use the app-user auth middleware accept either of the following credentials:

  • Authorization: Bearer <jwt> from the /api/v1/* auth endpoints
  • A valid Better Auth session cookie issued by /api/auth/*

These credentials resolve identities from app_users only. Admin users continue to authenticate through the Laravel/Filament admin surface and are not valid callers for the public /api/v1/auth/*, /api/auth/*, or /api/v1/me app-user routes.

For account creation and onboarding, see Registration.

Versioned JWT Routes

Login

http
POST /api/v1/auth/login

Request body:

json
{
	"email": "[email protected]",
	"password": "your-password"
}

Success response:

json
{
	"data": {
		"token": "<bearer-token>",
		"expiresAt": "<iso8601-expires-at>",
		"user": {
			"id": "usr_123",
			"email": "[email protected]",
			"name": "Jane Doe",
			"type": "user"
		}
	}
}

Errors

  • 401 invalid_credentials when the email or password is invalid, missing, malformed, or incorrect
  • 429 rate_limited after too many login attempts from the same resolved client key; if trusted proxy IP headers are disabled or unavailable, login throttling falls back to a shared bucket instead of trusting forwarded IP headers

Guest Session

http
POST /api/v1/auth/guest

Create an anonymous guest identity for read-only flows before account creation.

Response

json
{
	"data": {
		"token": "<bearer-token>",
		"expiresAt": "<iso8601-expires-at>",
		"user": {
			"id": "guest_<uuid>",
			"type": "guest"
		}
	}
}

Guest tokens expire after seven days.

Using Authenticated Routes

Send JWTs with the Authorization header when calling protected /api/v1/* routes:

http
Authorization: Bearer <token>

If your client already has a Better Auth browser session, the same middleware can resolve the authenticated app user from the session cookie without a bearer token.

Refresh Current JWT

http
POST /api/v1/auth/refresh
Authorization: Bearer <token>

Refresh is a sliding-window JWT refresh endpoint. It accepts a valid guest or user bearer token and returns a new token with a fresh expiry.

Response

json
{
	"data": {
		"token": "<new-bearer-token>",
		"expiresAt": "<iso8601-expires-at>"
	}
}

Errors

  • 401 auth_required when the bearer token is missing, expired, malformed, or refers to a deleted user

Current User

Read the Current User

http
GET /api/v1/me

This route returns the current app-user identity from either a bearer JWT or a Better Auth session cookie. The handle field is omitted until the user claims one.

Response for a User Session

json
{
	"data": {
		"user": {
			"id": "usr_123",
			"email": "[email protected]",
			"name": "Jane Doe",
			"handle": "janedoe",
			"type": "user",
			"createdAt": "<iso8601-created-at>"
		}
	}
}

Response for a Guest Session

json
{
	"data": {
		"user": {
			"id": "guest_123",
			"type": "guest"
		}
	}
}

Errors

  • 401 auth_required when no valid bearer token or session cookie is present, or the authenticated user no longer exists

Update the Current User

http
PATCH /api/v1/me

This route partially updates name and handle for the current authenticated user. It accepts a user bearer JWT or a Better Auth session cookie.

Request Body

json
{
	"name": "Jane Doe",
	"handle": "janedoe"
}

All fields are optional. Omitted fields remain unchanged, and handle values are normalized to lowercase before persistence.

Response

json
{
	"data": {
		"user": {
			"id": "usr_123",
			"email": "[email protected]",
			"name": "Jane Doe",
			"handle": "janedoe",
			"type": "user",
			"createdAt": "<iso8601-created-at>"
		}
	}
}

Errors

  • 400 invalid_body when the request body is malformed JSON or a JSON value other than an object
  • 401 auth_required when no valid bearer token or session cookie is present, or the authenticated user no longer exists
  • 403 forbidden when the caller is a guest identity
  • 409 handle_taken when the requested handle belongs to another user
  • 422 validation_error when name or handle fails validation

Current Scope

The following auth endpoints are implemented today:

  • POST /api/v1/auth/register
  • POST /api/v1/auth/login
  • POST /api/v1/auth/guest
  • POST /api/v1/auth/refresh
  • POST /api/v1/auth/phone/send-otp
  • POST /api/v1/auth/phone/verify-otp
  • POST /api/v1/auth/phone/resend-otp
  • GET /api/v1/me
  • PATCH /api/v1/me

Social OAuth flows use Better Auth native routes (not versioned /api/v1/auth/*):

  • POST /api/auth/sign-in/social — initiate Google or Facebook OAuth
  • GET /api/auth/callback/google — Google OAuth callback (handled by Better Auth)
  • GET /api/auth/callback/facebook — Facebook OAuth callback (handled by Better Auth)
  • POST /api/auth/sign-up/email — Better Auth email sign-up
  • POST /api/auth/sign-in/email — Better Auth email sign-in
  • GET /api/auth/get-session — Better Auth session lookup

Logout, account deletion, and additional provider-specific public routes are not part of the documented /api/v1/* contract yet.

Social OAuth

Better Auth handles Google and Facebook sign-in at the /api/auth/* surface. Clients should use the Better Auth SDK or follow the standard OAuth 2.0 flow.

Initiate Sign-In

http
POST /api/auth/sign-in/social
json
{
	"provider": "google",
	"callbackURL": "/dashboard"
}

Replace "google" with "facebook" to use the Facebook provider. Both providers return a redirect URL that the client must follow to complete the OAuth handshake.

Provider Setup

Configure the provider app to allow the Better Auth callback routes generated from BETTER_AUTH_URL:

  • Google: http://localhost:3001/api/auth/callback/google
  • Facebook: http://localhost:3001/api/auth/callback/facebook

For deployed environments, replace http://localhost:3001 with the API's public BETTER_AUTH_URL so the provider redirect URI exactly matches the callback route Better Auth serves.

Response

json
{
	"url": "https://accounts.google.com/o/oauth2/auth?...",
	"redirect": true
}

After the OAuth handshake completes, Better Auth establishes a session. Authenticated /api/v1/* routes accept either a Better Auth session cookie or a bearer JWT issued by the /api/v1/auth/* endpoints.

Phone OTP

Phone-number based authentication uses a one-time code delivered via SMS. The flow is:

  1. Call send-otp to deliver a code to the user's phone.
  2. Call verify-otp with the code to authenticate and receive a bearer token.
  3. Use resend-otp if the user needs the code resent (rate-limited identically to send-otp).

Send OTP

http
POST /api/v1/auth/phone/send-otp
Content-Type: application/json
json
{
	"phoneNumber": "+17678901234"
}

Responses: 200 { data: { sent: true } } | 400 invalid_body | 422 validation_error | 429 rate_limited

Verify OTP

http
POST /api/v1/auth/phone/verify-otp
Content-Type: application/json
json
{
	"phoneNumber": "+17678901234",
	"code": "123456"
}

Responses: 200 { data: { token, expiresAt, user } } | 400 invalid_body | 401 invalid_otp | 422 validation_error | 429 rate_limited

Resend OTP

http
POST /api/v1/auth/phone/resend-otp
Content-Type: application/json
json
{
	"phoneNumber": "+17678901234"
}

Responses: 200 { data: { sent: true } } | 400 invalid_body | 422 validation_error | 429 rate_limited

Rate limits: send-otp and resend-otp share a 5 requests per 10 minute window. verify-otp allows 10 requests per 10 minutes. These limits use the same resolved client key as the other auth routes; when trusted proxy headers are disabled or unavailable, the limiter falls back to a shared bucket instead of trusting forwarded IP headers.

Built with VitePress