Skip to content

API key permissions

When you create an API key in the Immich web UI (Account → API KeysNew 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.