Users & profiles
The signed-in user
GET /api/v1/user
Authorization: Bearer USER_TOKEN{
"data": {
"id": "01jexample…",
"name": "Ada King",
"username": "ada",
"email": "ada@example.com",
"email_verified_at": "2026-11-01T10:00:00.000000Z",
"avatar": "https://id.anikeen.com/avatars/01jexample…",
"locale": "en",
"profile_version": 8,
"billing_address_id": "01j…",
"vat_rate": 19,
"current_balance": 0,
"current_charges": 12.5,
"created_at": "…"
}
}The user as the user's own token sees them, including the VAT rate, balance and the open charges of the current month. Trusted apps also get role (admin or user). id is the user's Anikeen ID: store it to recognise the user.
avataris a stable URL: the picture behind it changes when the user changes it.profile_versiongrows with every change apps see (name, username, verified email, avatar). Compare it with theversionof theuser.updatedwebhook.
Scope: user:read (or user). A token without it gets only {"data": {"id": "…"}} once scopes are enforced (see Scopes): enough to check that the token still works, but no profile.
Linked social accounts
GET /api/v1/user/socialite-users lists the social logins linked to the account.
Changing the profile
With a token of the user carrying the scope user (* passes; user:read only reads), your app can change the user's name, username, email address and profile picture.
| Method | Path | Body |
|---|---|---|
PATCH | /api/v1/user/profile | {name?, username?, email?} – only the fields sent change |
POST | /api/v1/user/profile/avatar | multipart: avatar (file), optional crop[x], crop[y], crop[size] |
DELETE | /api/v1/user/profile/avatar | – removes the uploaded picture (back to the initials) |
curl -X PATCH https://id.anikeen.com/api/v1/user/profile \
-H 'Authorization: Bearer USER_TOKEN' \
-H 'Accept: application/json' -H 'Content-Type: application/json' \
-d '{"name": "Ada King", "email": "ada.new@example.com"}'Every 200 answer carries the user as GET /api/v1/user returns it, plus pending_email and pending_email_expires_at (both null without a pending change):
{"data": {"id": "01j…", "name": "Ada King", "email": "ada@example.com",
"profile_version": 9, "…": "…",
"pending_email": "ada.new@example.com", "pending_email_expires_at": "2026-11-16T10:00:00Z"}}// with the SDK
$profile = $user->updateProfile(['name' => 'Ada King', 'email' => 'ada.new@example.com']);
$profile->pendingEmail(); // "ada.new@example.com" until the user confirms it
$user->uploadAvatar($request->file('avatar'));
$user->uploadAvatar($path, ['x' => 120, 'y' => 40, 'size' => 512]);
$user->removeAvatar();Email addresses
A new email address never replaces the current one right away:
- Anikeen ID mails a confirmation link to the new address and a notice to the current one.
- The new address becomes the user's address only once they confirm it, signed in to Anikeen ID. An access token alone can't move the sign-in address.
- Until then, the current address stays valid for sign-in and mails.
user.updatedfollows the confirmation. - A new request replaces a pending one; a pending address is dropped after 7 days.
emailis lower-cased.
Profile pictures
jpeg, png, webp or gif, at most 5 MB and 64 to 8000 pixels per side. crop is a square in pixels of the upright picture; without it the centre is used. The avatar URL stays the same when the picture changes.
Limits
30 requests a minute per app and user; new email addresses 5 an hour and picture uploads 10 an hour per user (429 too_many_requests with Retry-After). Requests with an address that is taken count too.
Errors
Errors are {"message", "error"}. Validation errors also carry errors (messages per field) and codes (a code per field):
{
"message": "The username has already been taken.",
"error": "username_taken",
"errors": {"username": ["The username has already been taken."]},
"codes": {"username": "username_taken"}
}| Status | error |
|---|---|
| 401 | unauthenticated |
| 403 | missing_scope (no user scope), user_token_required (an app token: there is no user to change) |
| 413 | avatar_too_large (the upload is larger than the server accepts) |
| 422 | per field: name_invalid, username_taken, username_invalid, email_taken, email_invalid, avatar_invalid, avatar_too_large, crop_invalid (fields crop.x, crop.y, crop.size), else validation_failed |
| 429 | too_many_requests |
Usernames are 3–25 ASCII letters, digits, - and _, unique regardless of case.
Reading a user by ID (trusted apps)
Trusted apps can read the profile of a user connected to them with the app's own token, e.g. to catch up on a missed user.updated. No scope is needed.
GET /api/v1/client/users/{user}
Authorization: Bearer APP_TOKEN{
"data": {
"id": "01jexample…",
"name": "Ada King",
"username": "ada",
"email": "ada@example.com",
"email_verified_at": "2026-11-01T10:00:00.000000Z",
"avatar": "https://id.anikeen.com/avatars/01jexample…",
"locale": "en",
"profile_version": 8,
"created_at": "…",
"role": "user"
}
}The profile only: no balance, charges, payment method or security fields. The address is returned even when it isn't verified (email_verified_at is null then).
| Status | error |
|---|---|
| 403 | client_not_allowed (not a trusted app with its own token) |
| 404 | user_not_found (unknown, deleted or not connected to your app) |
| 429 | too_many_requests (120 requests a minute per app) |
$profile = app(\Anikeen\Id\AnikeenId::class)->findUserById($user->anikeen_id); // null: unknown, deleted or not connected
$profile?->name();
$profile?->email(); // returned even while unverified
$profile?->emailVerified();
$profile?->version(); // compare with the version of user.updated
$profile?->role(); // Role::ADMIN or Role::USER
$profile?->isAdmin();Keeping profiles in sync
Subscribe to the opt-in webhooks user.updated and user.deleted. Keep the highest version you have seen per user and ignore older events. On user.deleted, delete or anonymise your copy of the account.