Auth Flow

Full lifecycle for the auth-related routes: register, verify, login (single + multi-org), invite handling, password reset, and password change.

Base path: /api/v1/auth

This page is about the auth-flow endpoints. For the request-signing model (HMAC, JWT, dual-auth), see Authentication.


Register an organization

POST /api/v1/auth/register

Auth: Public.

Creates a new organization plus an owner user. Triggers a verification email.

Request body

Field Type Required Description
name string Yes Owner's full name
email string Yes Owner's email — must be unique
password string Yes Owner's password
business_name string Yes Organization legal name
website string No Org website URL
incorporation_country string No ISO country code
industry string No Free-form

Response

{
  "message": "Organization created. Check your email for the verification code.",
  "data": {
    "organization_id": "uuid",
    "email": "you@company.com"
  }
}

Verify email

POST /api/v1/auth/verify-email

Auth: Public.

Request body

Field Type Description
organization_id string From the register response
code string 6-digit code from the email

Resend verification code

POST /api/v1/auth/resend-code

Auth: Public.

Field Description
organization_id From the register response

Login

POST /api/v1/auth/login

Auth: Public.

Request body

{ "email": "you@company.com", "password": "..." }

Single-org response

{
  "message": "Login successful",
  "data": {
    "token": "eyJhbG...",
    "user": {
      "id": "uuid",
      "email": "you@company.com",
      "full_name": "Your Name"
    },
    "organization": {
      "id": "uuid",
      "name": "Your Company",
      "status": "active",
      "email_verified": true
    },
    "role": "owner",
    "pending_invites": []
  }
}

Use data.token as your JWT — send it in Authorization: Bearer <token> on all JWT-tier routes.

Multi-org response

If the user belongs to more than one organization, login does not pick one for you:

{
  "message": "Please select an organization",
  "requires_org_selection": true,
  "data": {
    "session_token": "<short-lived token>",
    "organizations": [
      { "id": "uuid", "name": "Org A", "role": "owner" },
      { "id": "uuid", "name": "Org B", "role": "developer" }
    ],
    "pending_invites": []
  }
}

Follow up with the org selection endpoint below.


Select an organization (multi-org login)

POST /api/v1/auth/select-org

Auth: Public (uses session_token from login).

Request body

Field Description
session_token From the multi-org login response
organization_id UUID of the chosen org

Response

{
  "message": "Organization selected",
  "data": {
    "token": "eyJhbG...",
    "user": { /* ... */ },
    "organization": { /* ... */ },
    "role": "owner"
  }
}

Switch organization (already logged in)

POST /api/v1/auth/switch-org

Auth: JWT.

For a user who's already authenticated and wants to swap into a different org they're a member of.

Field Description
organization_id Target org UUID

Returns a new JWT scoped to that org.


List my organizations

GET /api/v1/auth/my-organizations

Auth: JWT.

Returns every organization the current user belongs to, with their role in each.


Forgot / reset password

Request a reset

POST /api/v1/auth/forgot-password

Auth: Public.

Field Description
email The user's email

Sends a reset link with a token.

Validate the token (before showing the reset form)

GET /api/v1/auth/reset-password/:token

Auth: Public.

Returns 200 if the token is valid and unexpired, 4xx otherwise.

Reset the password

POST /api/v1/auth/reset-password/:token

Auth: Public.

Field Description
password The new password

Change password (logged in)

PUT /api/v1/auth/change-password

Auth: JWT.

Field Description
current_password Existing password
new_password New password

Invites

The invite lifecycle has two flows depending on whether the invitee already has a HasaPay account.

Get invite details

GET /api/v1/auth/invite/:token

Auth: Public.

Returns invite metadata so you can render a join page:

{
  "data": {
    "email": "newperson@company.com",
    "role": "developer",
    "role_display_name": "Developer",
    "organization_id": "uuid",
    "organization_name": "Org A",
    "inviter_name": "Existing Person",
    "expires_at": "2026-06-16T10:00:00Z",
    "is_expired": false,
    "existing_user": false
  }
}

existing_user tells you which accept route to call next.

Accept invite (new user — creates an account)

POST /api/v1/auth/invite/:token/accept

Auth: Public.

Field Description
full_name Their full name
password New account password

Creates the user, attaches them to the inviting org, returns a JWT for that org.

Accept invite (existing user)

POST /api/v1/auth/invite/:token/accept-existing

Auth: JWT.

No body. The currently-authenticated user joins the inviting org. Returns a new JWT scoped to the joined org.

Decline invite

POST /api/v1/auth/invite/:token/decline

Auth: JWT.

No body. Marks the invite as declined.

List my pending invites

GET /api/v1/auth/pending-invites

Auth: JWT.

Returns the invites that have been sent to the authenticated user's email and not yet accepted/declined.


Auth-flow quick reference

Flow Calls
New org signup register → email → verify-emaillogin
Owner re-login (single org) login
Multi-org user re-login loginselect-org
Switch orgs mid-session switch-org
New-user invite acceptance invite link → GET /auth/invite/:tokenPOST /auth/invite/:token/accept
Existing-user invite acceptance login → GET /auth/invite/:tokenPOST /auth/invite/:token/accept-existing
Forgot password forgot-password → email link → GET /auth/reset-password/:tokenPOST /auth/reset-password/:token
Change password (logged in) PUT /auth/change-password