API key permissions¶
When you create an API key in the Immich web UI (Account → API Keys
→ New API Key), you can limit the key to a specific set of
permissions. This page lists exactly which permissions pymmich
currently needs — both as a complete set (if you want one key that
works for every command) and broken down per command (if you want the
smallest key that's still useful).
Full access always works
If you don't care about scoping the key down, selecting All permissions on the API-key dialog works too. The permission list below is only interesting if you want to follow the principle of least privilege.
Complete set (all pymmich commands)¶
Selecting these nine permissions is enough for every command the tool currently ships:
| Permission | Used by |
|---|---|
album.read |
upload, download, list, share, unshare |
album.create |
upload (when an album needs to be created) |
albumAsset.create |
upload (associating uploaded assets with an album) |
albumUser.create |
share |
albumUser.delete |
unshare |
asset.read |
download, list (album contents, metadata search, per-asset details) |
asset.upload |
upload (uploading each file) |
asset.download |
download (streaming the original bytes) |
user.read |
share, unshare, list-users (listing / resolving users) |
The Immich endpoints GET /server/ping, GET /server/media-types and
GET /server/version are public and don't need any permission.
Same permissions on Immich 2.x and 3.x
The permission names above are identical on both server generations, so one key works for either. Some of the endpoints in the tables below do differ between Immich 2.x and 3.x — the tables say which is which, and Server compatibility has the full picture.
Per-command breakdown¶
GET /albums and its filters
Whenever a command matches album names, it lists the albums you
can see, which takes two requests on either server generation —
GET /albums plus GET /albums?shared=true on Immich 2.x, and
GET /albums?isOwned=true plus an unfiltered GET /albums on
Immich 3.x. pymmich merges the results, owned albums first.
Both cases need nothing but album.read.
pymmich upload¶
Permissions marked as "conditional" are only touched under specific circumstances — e.g. creating an album is only attempted when the destination album doesn't exist yet.
| Permission | HTTP endpoint | When |
|---|---|---|
album.read |
GET /albums |
Always (look up target album by name) |
album.create |
POST /albums |
Conditional: album did not exist yet |
asset.upload |
POST /assets |
For each file being uploaded |
albumAsset.create |
PUT /albums/{id}/assets |
When uploading a directory (= into album) |
Uploading only standalone files (no directories) does not require
album.read, album.create, or albumAsset.create, but those are
only skipped when no directory is in the argument list — the simplest
option is to just grant all four.
pymmich download¶
| Permission | HTTP endpoint | When |
|---|---|---|
album.read |
GET /albums — on Immich 2.x additionally GET /albums?shared=true |
Resolving target to album (owned + shared-with-me) |
asset.read |
POST /search/metadata; on Immich 2.x additionally GET /timeline/buckets, GET /timeline/bucket and GET /assets/{id} |
Album contents + filename search (+ per-asset details on 2.x) |
asset.download |
GET /assets/{id}/original |
For every asset being downloaded |
Why timeline instead of search for album contents on Immich 2.x?
On Immich 2.x, POST /search/metadata forcibly scopes results to
assets owned by the caller (and their partners). For shared
albums, that hides photos contributed by other members. The
timeline endpoints don't apply that filter when an albumId is
given, so pymmich uses them there to get the full roster —
at the price of one GET /assets/{id} per asset, because the
timeline payload carries no filenames.
Immich 3.0 stopped applying the owner scope when an albumIds
filter is given (it checks album read access instead). On 3.x
pymmich therefore reads album contents with a plain
POST /search/metadata and touches neither the timeline endpoints
nor GET /assets/{id}. Either way asset.read is the permission
involved. See
Server compatibility.
pymmich list¶
| Permission | HTTP endpoint | When |
|---|---|---|
album.read |
GET /albums — with a shared=true (Immich 2.x) or isOwned/isShared (Immich 3.x) filter, depending on --only-owned / --only-shared |
Album matching and --albums-only listings |
asset.read |
POST /search/metadata; on Immich 2.x additionally GET /timeline/buckets, GET /timeline/bucket and GET /assets/{id} |
Asset listings, filename search, album asset enumeration |
pymmich list-users¶
| Permission | HTTP endpoint | When |
|---|---|---|
user.read |
GET /users |
Always (the command's whole job). |
No additional permission beyond what share and unshare already
need — if those work, list-users works too.
pymmich share¶
| Permission | HTTP endpoint | When |
|---|---|---|
user.read |
GET /users |
Resolving --with identifiers |
album.read |
GET /albums |
Matching album glob patterns |
albumUser.create |
PUT /albums/{id}/users |
For each matched album |
pymmich unshare¶
| Permission | HTTP endpoint | When |
|---|---|---|
user.read |
GET /users |
Resolving --with identifiers |
album.read |
GET /albums + GET /albums/{id} |
Matching globs + reading current shares |
albumUser.delete |
DELETE /albums/{id}/user/{userId} |
For each user actually removed |
Troubleshooting¶
If pymmich fails with an HTTP 403 ("forbidden") response, the API
key you're using is missing one of the permissions listed above. The
error message that pymmich prints includes the HTTP method and path
that was rejected — look that endpoint up in the tables above to see
which permission to add.