UniSave uses a JWT access token + opaque refresh token model. Access tokens are short-lived; refresh tokens are rotated on every use.
Authentication Flow
Create Anonymous User
Start a new session without any credentials. Returns a fresh user with tokens.
curl -X POST https://api.unisave.io/v1/auth/anonymous
Response 200
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "opaque-refresh-token-string",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"isAnonymous": true,
"displayName": null,
"email": null,
"photoUrl": null,
"isPro": false
}
}
OAuth Sign-In / Link
OAuth endpoints serve dual purpose based on whether an access token is provided:
- Without token → Sign-in: Creates or retrieves the user associated with the OAuth identity
- With token +
link=true → Link: Attaches the OAuth identity to the current user
Google OAuth
# Sign-in (no token)
curl -X POST https://api.unisave.io/v1/auth/oauth/google \
-H "Content-Type: application/json" \
-d '{"idToken": "google-id-token-from-client"}'
# Link to current user
curl -X POST https://api.unisave.io/v1/auth/oauth/google \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"idToken": "google-id-token", "link": true}'
Apple OAuth
# Sign-in
curl -X POST https://api.unisave.io/v1/auth/oauth/apple \
-H "Content-Type: application/json" \
-d '{"identityToken": "apple-identity-token", "authorizationCode": "code"}'
# Link to current user
curl -X POST https://api.unisave.io/v1/auth/oauth/apple \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"identityToken": "apple-identity-token", "authorizationCode": "code", "link": true}'
Response 200 — same shape as anonymous auth.
Identity Conflict (Merge)
If the OAuth identity already belongs to another user and you’re linking:
- The backend signs into the existing user (owner of the identity)
- Bookmarks from the anonymous account are merged into the target account (best-effort)
- The anonymous account is cleaned up
If merge isn’t possible, the API returns 409 identity-already-in-use.
Refresh Tokens
Access tokens expire. Use the refresh endpoint to get new ones. Refresh tokens are single-use — each call returns a new pair.
curl -X POST https://api.unisave.io/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "current-refresh-token"}'
Response 200
{
"accessToken": "new-jwt-access-token",
"refreshToken": "new-opaque-refresh-token",
"user": { ... }
}
Refresh tokens rotate on every use. If you use an old refresh token, it’s rejected. Store the latest refresh token securely (Keychain on iOS, chrome.storage.local in the extension).
Logout
Revokes the refresh token server-side. The access token remains valid until expiry but new tokens cannot be minted.
curl -X POST https://api.unisave.io/v1/auth/logout \
-H "Content-Type: application/json" \
-d '{"refreshToken": "current-refresh-token"}'
Response 204 No Content
User Profile
Get the current user’s profile and subscription status.
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://api.unisave.io/v1/me
Response 200
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"isAnonymous": false,
"displayName": "Justin",
"email": "justin@example.com",
"photoUrl": "https://...",
"isPro": true
}
Delete Account
Permanently deletes the user account and all associated data (bookmarks, collections, enrichment jobs, subscriptions). This action is irreversible.
curl -X DELETE -H "Authorization: Bearer $ACCESS_TOKEN" \
https://api.unisave.io/v1/account
Response 204 No Content