immich-app/immich · error · BadRequestException
Live photo video does not belong to the user
Error message
Live photo video does not belong to the user
What it means
Thrown by onBeforeLink when the motion video asset exists and is a video, but its ownerId differs from the userId performing the link. Users may only attach live photo motion videos they own; this enforces per-user asset ownership.
Solutions
- Ensure the motion video is uploaded by the same user performing the link
- Log in as the owner user or run the operation with the owner's credentials
- Copy/upload the motion video under the correct user account before linking
- If legitimate sharing is needed, use the official share flow rather than direct linking
Example fix
// before
await onBeforeLink(repos, { userId: me.id, livePhotoVideoId: partnersVideoId });
// after
const motion = await assetRepository.getById(partnersVideoId);
if (motion && motion.ownerId === me.id) {
await onBeforeLink(repos, { userId: me.id, livePhotoVideoId: partnersVideoId });
} else {
throw new Error('Motion video must be owned by the linking user');
} Defensive patterns
Strategy: validation
Validate before calling
if (motionAsset.ownerId !== currentUserId) {
throw new Error('Motion video must be owned by the linking user');
} Try / catch
try {
await linkLivePhoto(assetId, livePhotoVideoId);
} catch (e) {
if (e.status === 400 && /does not belong/.test(e.message)) {
// re-upload the video under the current user and retry
}
} Prevention
- Authenticate as the owner user when running bulk live photo jobs
- Never mix credentials across users in automation scripts
- Compare ownerId on both assets before linking
When it happens
Trigger: A user attempts to link a motion video uploaded by another user (e.g. shared asset), or an admin job runs with a mismatched user context.
Common situations: Using a shared album partner's video as your photo's motion component; automation scripts that mix up user credentials; duplicated users after account merge.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Album must have an owner
- Asset not found
- Asset not found
- Asset not found or asset is not a video
- Both assets must exist
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/c461e7ff7cb43b1b.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/utils/asset.util.ts:157
return [...partnerIds];
};
export type AssetHookRepositories = { asset: AssetRepository; event: EventRepository };
export const onBeforeLink = async (
{ asset: assetRepository, event: eventRepository }: AssetHookRepositories,
{ userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
) => {
const motionAsset = await assetRepository.getById(livePhotoVideoId);
if (!motionAsset) {
throw new BadRequestException('Live photo video not found');
}
if (motionAsset.type !== AssetType.Video) {
throw new BadRequestException('Live photo video must be a video');
}
if (motionAsset.ownerId !== userId) {
throw new BadRequestException('Live photo video does not belong to the user');
}
if (motionAsset && motionAsset.visibility === AssetVisibility.Timeline) {
await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.Hidden });
await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });
}
};
export const onBeforeUnlink = async (
{ asset: assetRepository }: AssetHookRepositories,
{ livePhotoVideoId }: { livePhotoVideoId: string },
) => {
const motion = await assetRepository.getById(livePhotoVideoId);
if (!motion) {
return null;
}
if (StorageCore.isAndroidMotionPath(motion.originalPath)) {View on GitHub (pinned to e55ac299a4)