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:
| 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:
| 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:
Response: 200 OK
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:
Request Body:
Response: 200 OK
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:
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:
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:
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:
Response: 200 OK
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:
Request Body:
| 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:
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:
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) |