Skip to content

Authentication API

All authentication endpoints live under the /auth prefix. These handle user registration, login, OAuth, session management, and profile updates.

Base URL

The base URL for every endpoint on this page is https://auth.iminklet.com. Example: POST https://auth.iminklet.com/auth/register.

Token Sliding Renewal

Access tokens expire after 72 hours by default. When an access token has less than 24 hours remaining, the backend automatically issues a fresh token in the X-Renewed-Token response header on every authenticated request. Clients should check this header and replace the stored token when it is present. See the overview page for details.

Endpoints


POST /auth/register

Create a new user account.

Request Body:

{
  "email": "user@example.com",
  "username": "inkuser",
  "password": "securePassword123"
}
Field Type Required Description
email string Yes Valid email address
username string Yes Unique username
password string Yes Minimum 8 characters

Response: 201 Created

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
  "user": {
    "id": "01912345-6789-7abc-def0-123456789abc",
    "email": "user@example.com",
    "username": "inkuser",
    "createdAt": "2026-01-15T10:30:00Z",
    "updatedAt": "2026-01-15T10:30:00Z"
  }
}

Errors:

Code Cause
400 Missing or invalid fields
409 Email or username already taken

POST /auth/login

Authenticate with email or username and password.

Request Body:

{
  "identifier": "user@example.com",
  "password": "securePassword123"
}
Field Type Required Description
identifier string Yes Email address or username
password string Yes Account password

Response: 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
  "user": {
    "id": "01912345-6789-7abc-def0-123456789abc",
    "email": "user@example.com",
    "username": "inkuser",
    "createdAt": "2026-01-15T10:30:00Z",
    "updatedAt": "2026-01-15T10:30:00Z"
  }
}

Errors:

Code Cause
400 Missing fields
401 Invalid credentials

POST /auth/refresh

Exchange a valid refresh token for a new token pair. The old refresh token is invalidated.

Request Body:

{
  "refreshToken": "dGhpcyBpcyBhIHJlZnJl..."
}

Response: 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "bmV3IHJlZnJlc2ggdG9r..."
}

Errors:

Code Cause
401 Refresh token is invalid, expired, or already used

Token Rotation

Refresh tokens are single-use. Each call to this endpoint invalidates the previous refresh token and returns a new pair. If a refresh token is reused, all sessions for the user may be revoked as a security measure.


POST /auth/logout

Requires authentication

Revoke the provided refresh token, ending the session.

Request Headers:

Authorization: Bearer {accessToken}

Request Body:

{
  "refreshToken": "dGhpcyBpcyBhIHJlZnJl..."
}

Response: 200 OK

{
  "message": "logged out"
}

Errors:

Code Cause
401 Missing or invalid access token

GET /auth/me

Requires authentication

Retrieve the authenticated user's profile, including linked OAuth accounts and subscription status.

Request Headers:

Authorization: Bearer {accessToken}

Response: 200 OK

{
  "id": "01912345-6789-7abc-def0-123456789abc",
  "email": "user@example.com",
  "username": "inkuser",
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-01-15T10:30:00Z",
  "oauthAccounts": [
    {
      "provider": "google",
      "providerUserId": "117382948271639",
      "email": "user@gmail.com",
      "linkedAt": "2026-01-20T14:00:00Z"
    }
  ],
  "subscription": {
    "status": "active",
    "plan": "pro",
    "interval": "monthly",
    "currentPeriodEnd": "2026-02-15T10:30:00Z",
    "cancelAtPeriodEnd": false
  }
}

Tip

The subscription field is null if the user has never subscribed.

Errors:

Code Cause
401 Missing or invalid access token

PUT /auth/me

Requires authentication

Update the authenticated user's profile. All fields are optional --- only include fields you want to change.

Request Headers:

Authorization: Bearer {accessToken}

Request Body:

{
  "username": "newusername",
  "password": "newSecurePassword456",
  "currentPassword": "securePassword123"
}
Field Type Required Description
username string No New username
password string No New password (min 8 characters)
currentPassword string Conditional Required when changing password

Response: 200 OK

{
  "id": "01912345-6789-7abc-def0-123456789abc",
  "email": "user@example.com",
  "username": "newusername",
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-01-16T08:00:00Z"
}

Errors:

Code Cause
400 Invalid fields or currentPassword missing when changing password
401 Missing or invalid access token
409 Username already taken

GET /auth/sessions

Requires authentication

List all active sessions for the authenticated user.

Request Headers:

Authorization: Bearer {accessToken}

Response: 200 OK

[
  {
    "id": "01912345-0000-7abc-def0-000000000001",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
    "ip": "192.168.1.100",
    "createdAt": "2026-01-15T10:30:00Z",
    "lastUsedAt": "2026-01-16T08:00:00Z"
  },
  {
    "id": "01912345-0000-7abc-def0-000000000002",
    "userAgent": "InkletDesktop/1.0",
    "ip": "10.0.0.50",
    "createdAt": "2026-01-14T09:00:00Z",
    "lastUsedAt": "2026-01-15T22:00:00Z"
  }
]

Errors:

Code Cause
401 Missing or invalid access token

DELETE /auth/sessions/{id}

Requires authentication

Revoke a specific session by its ID. This invalidates the refresh token associated with that session.

Path Parameters:

Parameter Description
id Session UUID

Request Headers:

Authorization: Bearer {accessToken}

Response: 200 OK

{
  "message": "session revoked"
}

Errors:

Code Cause
401 Missing or invalid access token
404 Session not found or does not belong to user

DELETE /auth/account

Requires authentication

Permanently deletes the authenticated user's account and all associated data. This action is irreversible. Satisfies App Store Guideline 5.1.1(v) account deletion requirements.

Request Headers:

Authorization: Bearer {accessToken}

Request Body:

{
  "password": "currentPassword123",
  "confirm": true
}
Field Type Required Description
confirm boolean Yes Must be true as explicit confirmation of intent to delete
password string Conditional Required for users who have a password set; OAuth-only users (no password) may omit this field

Response: 204 No Content

Successful deletion; response has no body.

Behavior:

  • Hard-deletes all data associated with the user in a single transaction: sessions, oauth_accounts, subscriptions, files, raw_upload_items, render_tasks, content_blocks, pushes for the user's devices, and the users row
  • Devices are unlinked, not deleted β€” they revert to a claimable state (devices are shared hardware)
  • Before deletion: Apple Sign in authorization is revoked and active Stripe subscriptions are cancelled

Errors:

Code Cause
400 confirm is not true or request body is malformed
401 Missing or invalid access token, or incorrect password
404 User no longer exists (endpoint is idempotent)
500 Internal error (e.g., Apple revocation or Stripe cancellation failed)

Irreversible Action

This endpoint permanently deletes the account and all associated data with no recovery path. Clients must present a clear confirmation prompt to the user before calling this endpoint.


OAuth Endpoints

Inklet supports Sign in with Google and Sign in with Apple. The OAuth flow uses server-side redirects.

GET /auth/oauth/google

Initiate Google OAuth sign-in. The client query parameter determines which redirect URI to use.

Query Parameters:

Parameter Values Description
client web, desktop, ios Client platform initiating the flow

Example:

GET /auth/oauth/google?client=web

Response: 302 Found --- redirects to Google's OAuth consent screen.


GET /auth/oauth/google/callback

Handles the OAuth callback from Google. This endpoint is not called directly by clients --- Google redirects here after the user authorizes.

Behavior:

  • Creates a new account if the Google email is not registered
  • Links the Google account if the email matches an existing user
  • Returns tokens via redirect (with tokens as query parameters or fragment, depending on client)

GET /auth/oauth/apple

Initiate Apple OAuth sign-in.

Query Parameters:

Parameter Values Description
client web, desktop, ios Client platform initiating the flow

Example:

GET /auth/oauth/apple?client=web

Response: 302 Found --- redirects to Apple's OAuth consent screen.


POST /auth/oauth/apple/callback

Handles the OAuth callback from Apple. Apple uses form_post response mode, so this endpoint receives a POST with form-encoded data rather than query parameters.

Apple OAuth Differences

Unlike Google, Apple sends the callback as a POST request with application/x-www-form-urlencoded body. Apple also only provides the user's name on the first authorization --- subsequent sign-ins only include the email.

Behavior:

  • Creates a new account if the Apple email is not registered
  • Links the Apple account if the email matches an existing user
  • Returns tokens via redirect

POST /auth/oauth/apple/native

Sign in with Apple for native iOS apps. Unlike the web callback flow, the iOS app completes Apple authentication on-device and posts the credentials directly to this endpoint. The backend verifies the Apple Identity Token and returns an Inklet token pair (same shape as POST /auth/login β€” no redirect).

Request Body:

{
  "identityToken": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiRVMyNTYifQ...",
  "authorizationCode": "c0e0a10d8f8f44b...",
  "nonce": "abc123",
  "fullName": {
    "givenName": "John",
    "familyName": "Doe"
  }
}
Field Type Required Description
identityToken string Yes Apple-issued JWT Identity Token; the backend verifies its signature against Apple's public keys
authorizationCode string No One-time authorization code; the backend exchanges it for an Apple Refresh Token for future account revocation
nonce string No Client-generated replay-protection nonce; if provided, it must match the nonce in the Identity Token
fullName.givenName string No User's given name (only returned by Apple on first authorization)
fullName.familyName string No User's family name (only returned by Apple on first authorization)

Response: 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
  "user": {
    "id": "01912345-6789-7abc-def0-123456789abc",
    "email": "user@privaterelay.appleid.com",
    "username": "johndoe1a2b3c4d",
    "createdAt": "2026-01-15T10:30:00Z",
    "updatedAt": "2026-01-15T10:30:00Z"
  }
}

First vs. Subsequent Sign-ins

Apple only provides the user's name on the first authorization. On subsequent sign-ins fullName is empty, and the username is derived from the email's local part with a random suffix appended.

Errors:

Code Cause
400 Invalid request body or identityToken missing
401 Apple Identity Token signature invalid or nonce mismatch
501 Backend not configured with APPLE_BUNDLE_ID (native flow unavailable)