> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tipstack.fun/llms.txt
> Use this file to discover all available pages before exploring further.

# Authenticate with Tip Stack: Registration, Login, and OTP

> Register, log in with password or OTP, and authenticate SDK requests with Bearer API keys. Full credential guide for the Tip Stack API.

Before you can call most Tip Stack endpoints, you need to prove who you are. Tip Stack uses two complementary authentication patterns: session-based auth for user-facing flows (registration, login, profile, payouts) and API key auth for server-side SDK and embed integrations. This page walks you through both patterns and all of the related endpoints.

## Two Auth Patterns

| Pattern            | How to pass credentials              | Used for                               |
| ------------------ | ------------------------------------ | -------------------------------------- |
| **Session cookie** | Cookie set automatically on login    | `/auth/*`, `/payouts/*`, `/creators/*` |
| **Bearer API key** | `Authorization: Bearer YOUR_API_KEY` | `/sdk/init`, `/sdk/tip`, `/sdk/events` |

***

## Register

Create a new Tip Stack account. A session is established immediately on success — you do not need a separate login call.

```http theme={null}
POST /auth/register
```

### Request body

<ParamField body="email" type="string" required>
  A valid email address. It must not already be registered on the platform.
</ParamField>

<ParamField body="password" type="string" required>
  Account password. Minimum 8 characters.
</ParamField>

<ParamField body="name" type="string" required>
  Display name for the account. Must be between 2 and 100 characters.
</ParamField>

```json theme={null}
{
  "email": "alice@example.com",
  "password": "mypassword123",
  "name": "Alice"
}
```

### Response `200`

<ResponseField name="success" type="boolean">
  `true` on a successful registration.
</ResponseField>

<ResponseField name="user" type="object">
  The newly created user record.

  <Expandable title="user fields">
    <ResponseField name="id" type="string">UUID of the user.</ResponseField>
    <ResponseField name="email" type="string">Email address.</ResponseField>
    <ResponseField name="name" type="string">Display name.</ResponseField>
    <ResponseField name="full_name" type="string">Same as `name`; included for compatibility.</ResponseField>
    <ResponseField name="first_name" type="string | null">First word of `name`, or `null` if name is not set.</ResponseField>

    <ResponseField name="roles" type="array">
      Assigned roles, e.g. `["user"]`.
    </ResponseField>

    <ResponseField name="walletAddress" type="string | null">
      Masked Solana wallet address (`"XXXX...XXXX"`). May be `null` while the wallet is being provisioned.
    </ResponseField>

    <ResponseField name="walletPublicKey" type="string | null">
      Full Solana wallet public key. May be `null` while the wallet is being provisioned.
    </ResponseField>

    <ResponseField name="emailVerifiedAt" type="string | null">
      ISO timestamp of email verification, or `null` if not yet verified.
    </ResponseField>

    <ResponseField name="onboardingComplete" type="boolean">
      Whether the user has completed onboarding.
    </ResponseField>

    <ResponseField name="onboarding_complete" type="boolean">
      Same as `onboardingComplete`; included for compatibility.
    </ResponseField>

    <ResponseField name="solDomain" type="string | null">`.sol` domain, if linked.</ResponseField>
    <ResponseField name="twitterHandle" type="string | null">Linked Twitter/X handle.</ResponseField>
    <ResponseField name="discordHandle" type="string | null">Linked Discord handle.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="auth" type="object">
  Session credentials.

  <Expandable title="auth fields">
    <ResponseField name="accessToken" type="string">Bearer token for authenticated requests.</ResponseField>
    <ResponseField name="tokenType" type="string">`"Bearer"`</ResponseField>
    <ResponseField name="expiresAt" type="string">ISO timestamp of token expiry.</ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "success": true,
  "user": {
    "id": "a1b2c3d4-...",
    "email": "alice@example.com",
    "name": "Alice",
    "full_name": "Alice",
    "first_name": "Alice",
    "roles": ["user"],
    "walletAddress": "So1a...ana1",
    "walletPublicKey": "So1ana...",
    "emailVerifiedAt": null,
    "onboardingComplete": false,
    "onboarding_complete": false,
    "solDomain": null,
    "twitterHandle": null,
    "discordHandle": null
  },
  "auth": {
    "accessToken": "eyJ...",
    "tokenType": "Bearer",
    "expiresAt": "2025-12-31T00:00:00.000Z"
  }
}
```

**Error responses**

| Status | `error` value                                                                     | Cause              |
| ------ | --------------------------------------------------------------------------------- | ------------------ |
| `400`  | `"Invalid payload: Name must be 2-100 characters and password min 8 characters."` | Validation failure |
| `409`  | `"Email already in use"`                                                          | Duplicate account  |

***

## Login

Log in with an existing email and password. The server sets a session cookie and returns an access token.

```http theme={null}
POST /auth/login
```

### Request body

<ParamField body="email" type="string" required>
  The registered email address.
</ParamField>

<ParamField body="password" type="string" required>
  The account password.
</ParamField>

```json theme={null}
{
  "email": "alice@example.com",
  "password": "mypassword123"
}
```

### Response `200`

Returns the same `user` and `auth` shape as [Register](#register) above, populated with your existing account data.

```json theme={null}
{
  "success": true,
  "user": {
    "id": "a1b2c3d4-...",
    "email": "alice@example.com",
    "name": "Alice",
    "full_name": "Alice",
    "first_name": "Alice",
    "roles": ["user"],
    "walletAddress": "So1a...ana1",
    "walletPublicKey": "So1ana...",
    "emailVerifiedAt": "2025-01-15T10:30:00.000Z",
    "onboardingComplete": true,
    "onboarding_complete": true,
    "solDomain": "alice",
    "twitterHandle": "@alice",
    "discordHandle": null
  },
  "auth": {
    "accessToken": "eyJ...",
    "tokenType": "Bearer",
    "expiresAt": "2025-12-31T00:00:00.000Z"
  }
}
```

**Error responses**

| Status | `error` value           | Cause                           |
| ------ | ----------------------- | ------------------------------- |
| `400`  | `"Invalid payload"`     | Missing email or password       |
| `401`  | `"Invalid credentials"` | Wrong password or unknown email |

***

## Get Current User

Fetch the profile of the currently authenticated user. Use this to verify a session is still valid or to refresh user data in your application.

```http theme={null}
GET /auth/me
```

This endpoint requires a valid session cookie or `Authorization: Bearer <accessToken>` header. No request body is needed.

### Response `200`

<ResponseField name="success" type="boolean">
  `true` when the session is valid.
</ResponseField>

<ResponseField name="user" type="object">
  Full profile of the authenticated user.

  <Expandable title="user fields">
    <ResponseField name="id" type="string">User UUID.</ResponseField>
    <ResponseField name="name" type="string">Display name.</ResponseField>
    <ResponseField name="email" type="string">Email address.</ResponseField>

    <ResponseField name="walletAddress" type="string | null">
      Masked Solana wallet address (`"XXXX...XXXX"`).
    </ResponseField>

    <ResponseField name="walletPublicKey" type="string | null">
      Full Solana wallet public key.
    </ResponseField>

    <ResponseField name="roles" type="array">List of assigned roles.</ResponseField>
    <ResponseField name="solDomain" type="string | null">`.sol` domain, if linked.</ResponseField>
    <ResponseField name="twitterHandle" type="string | null">Linked Twitter/X handle.</ResponseField>
    <ResponseField name="discordHandle" type="string | null">Linked Discord handle.</ResponseField>
    <ResponseField name="emailVerifiedAt" type="string | null">ISO timestamp of email verification.</ResponseField>
    <ResponseField name="onboardingComplete" type="boolean">Onboarding status.</ResponseField>
    <ResponseField name="onboarding_complete" type="boolean">Same as `onboardingComplete`; included for compatibility.</ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "success": true,
  "user": {
    "id": "a1b2c3d4-...",
    "name": "Alice",
    "email": "alice@example.com",
    "walletAddress": "So1a...ana1",
    "walletPublicKey": "So1ana...",
    "roles": ["user"],
    "solDomain": "alice",
    "twitterHandle": "@alice",
    "discordHandle": null,
    "emailVerifiedAt": "2025-01-15T10:30:00.000Z",
    "onboardingComplete": true,
    "onboarding_complete": true
  }
}
```

**Error responses**

| Status | `error` value    | Cause                  |
| ------ | ---------------- | ---------------------- |
| `401`  | `"Unauthorized"` | No valid session found |

***

## OTP (Email Code) Login

Tip Stack supports a passwordless login flow using a one-time code sent to the user's email. Use this as an alternative to password-based login, or as a fallback for users who have not set a password.

<Steps>
  <Step title="Send the code">
    Post the user's email to start the OTP flow. Tip Stack generates a 6-digit code that expires in 10 minutes and emails it to the address.

    ```http theme={null}
    POST /auth/otp/start
    ```

    <ParamField body="email" type="string" required>
      The email address to send the one-time code to.
    </ParamField>

    ```json theme={null}
    { "email": "alice@example.com" }
    ```

    **Response `200`**

    ```json theme={null}
    { "success": true }
    ```

    | Status | `error` value         | Cause                        |
    | ------ | --------------------- | ---------------------------- |
    | `400`  | `"Email required"`    | No email provided            |
    | `404`  | `"Account not found"` | No account matches the email |
  </Step>

  <Step title="Verify the code">
    Submit the email and the 6-digit code the user received. On success, Tip Stack creates a session and returns the same `user` and `auth` payload as a standard login.

    ```http theme={null}
    POST /auth/otp/verify
    ```

    <ParamField body="email" type="string" required>
      The same email address used in `/auth/otp/start`.
    </ParamField>

    <ParamField body="code" type="string" required>
      The 6-digit one-time code from the email.
    </ParamField>

    ```json theme={null}
    {
      "email": "alice@example.com",
      "code": "847291"
    }
    ```

    **Response `200`**

    ```json theme={null}
    {
      "success": true,
      "user": {
        "id": "a1b2c3d4-...",
        "email": "alice@example.com",
        "name": "Alice",
        "full_name": "Alice",
        "roles": ["user"],
        "emailVerifiedAt": "2025-01-15T10:30:00.000Z",
        "onboardingComplete": true,
        "solDomain": "alice",
        "twitterHandle": "@alice",
        "discordHandle": null
      },
      "auth": { "accessToken": "eyJ...", "tokenType": "Bearer", "expiresAt": "..." }
    }
    ```

    | Status | `error` value                                                | Cause                                       |
    | ------ | ------------------------------------------------------------ | ------------------------------------------- |
    | `400`  | `"Invalid or expired code"`                                  | Wrong code or code past 10-minute expiry    |
    | `429`  | `"Too many failed attempts. Account locked for 15 minutes."` | 5 failed attempts within the lockout window |
  </Step>
</Steps>

<Note>
  Each one-time code is valid for 10 minutes. After a successful verification the code is immediately invalidated and cannot be reused.
</Note>

***

## SDK API Key Auth

SDK endpoints (`/sdk/init`, `/sdk/tip`, `/sdk/events`) are designed for server-side integrations and embedded widgets. They do not use session cookies. Instead, pass your API key as a Bearer token on every request.

```http theme={null}
Authorization: Bearer YOUR_API_KEY
```

You can find your API key in your creator dashboard under **Settings → API Keys**.

<Warning>
  Never expose your API key in client-side JavaScript. Make all `/sdk/init` calls from your backend only — the key grants full SDK access on your behalf.
</Warning>

<Note>
  SDK endpoints validate your API key server-side. Use a key generated from your dashboard — do not share it publicly or commit it to version control.
</Note>

### Initializing an SDK session

Call `/sdk/init` from your backend to create a short-lived session token for a tipping widget embed. Pass the token to your frontend instead of your raw API key.

```http theme={null}
POST /sdk/init
Authorization: Bearer YOUR_API_KEY
```

<ParamField body="creatorId" type="string" required>
  The creator's wallet address, `.sol` domain, or UUID (prefixed with `auth_`).
</ParamField>

<ParamField body="originUrl" type="string" required>
  The URL of the page embedding the widget. This must match a domain you have whitelisted in your creator dashboard. `localhost` and `127.0.0.1` are always permitted for local development.
</ParamField>

<ParamField body="theme" type="string">
  Widget theme. Defaults to `"dark"`. Pass `"light"` for a light theme.
</ParamField>

```json theme={null}
{
  "creatorId": "alice.sol",
  "originUrl": "https://yourdomain.com",
  "theme": "dark"
}
```

**Response `200`**

<ResponseField name="sessionToken" type="string">
  A short-lived token in the format `sdk_sess_<uuid>`. Use this as the Bearer value for `/sdk/tip` calls from the frontend.
</ResponseField>

<ResponseField name="config" type="object">
  Widget configuration to pass to the embedded client.

  <Expandable title="config fields">
    <ResponseField name="creatorId" type="string">Resolved UUID of the creator.</ResponseField>
    <ResponseField name="creatorAddress" type="string">Masked wallet address (first 4 chars + `...`).</ResponseField>
    <ResponseField name="acceptedTokens" type="array">Tokens the creator accepts, e.g. `["SOL", "USDC"]`.</ResponseField>
    <ResponseField name="embedUrl" type="string">Pre-built checkout URL for iframe embeds.</ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "success": true,
  "sessionToken": "sdk_sess_f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "config": {
    "creatorId": "a1b2c3d4-...",
    "creatorAddress": "So1a...",
    "acceptedTokens": ["SOL", "USDC"],
    "embedUrl": "https://tipstack.fun/checkout/a1b2c3d4-...?theme=dark"
  }
}
```

| Status | `error` value                            | Cause                                    |
| ------ | ---------------------------------------- | ---------------------------------------- |
| `400`  | `"creatorId and originUrl are required"` | Missing required fields                  |
| `401`  | `"Missing or invalid API key"`           | No Bearer token or key too short         |
| `403`  | `"Unauthorized Origin"`                  | `originUrl` is not whitelisted           |
| `404`  | `"Creator not found"`                    | No creator matches the given `creatorId` |

### Using the session token

Once your backend has received a `sessionToken` from `/sdk/init`, pass it to your frontend and use it as the Bearer token for `/sdk/tip` calls:

```http theme={null}
Authorization: Bearer sdk_sess_<uuid>
```

```javascript theme={null}
// Safe — session token in the browser, API key stays on the server
const response = await fetch("https://tipstack.fun/api/sdk/tip", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${sessionToken}`,
  },
  body: JSON.stringify({ amount: 0.5, token: "SOL" }),
});
```

<Tip>
  Treat `sessionToken` values as short-lived and single-use. Re-initialize a session by calling `/sdk/init` again from your backend whenever you render a new embed instance.
</Tip>
