Media uploads
Image bytes never pass through the litecommerce API. Instead you mint a
signed upload URL, PUT the file straight to storage, then confirm the
upload to attach metadata. Three steps. Examples reuse the
api() helper.
1. Mint a signed URL
POST /merchant/uploads/signed-url — permission: catalog:write. Owner/Admin
retain broad access; Staff needs that group.
const {
imageId,
uploadUrl,
token: uploadToken, // Supabase upload token — pair with storagePath in step 2
bucket,
publicUrl,
storagePath,
} = await api<{
imageId: string;
uploadUrl: string;
token: string;
bucket: string;
publicUrl: string;
storagePath: string;
}>("/merchant/uploads/signed-url", {
method: "POST",
token, // merchant bearer (the api() helper's option)
body: JSON.stringify({
ownerType: "item", // "item" | "collection"
ownerId: itemId, // must belong to your tenant (else 404)
filename: "trail-pack.jpg",
mimeType: "image/jpeg", // jpeg | png | webp | avif
bytes: 482113, // size; capped at 15 MB by default
}),
});The service verifies your org owns the target item/collection before issuing
the URL, and computes publicUrl itself. The signed URL is short-lived
(minutes).
2. Upload the file to storage
Upload the bytes with the Supabase Storage client's uploadToSignedUrl
helper, passing the storagePath + token from step 1. (A raw PUT against
the signed URL works in some environments but isn't the SDK-blessed path —
use the helper.)
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
await supabase.storage
.from(bucket)
.uploadToSignedUrl(storagePath, uploadToken, fileBlob);3. Confirm the upload
Attach the image to the item (or collection) so it shows up in reads:
await api(`/merchant/items/${itemId}/images`, {
method: "POST",
token, // merchant bearer
body: JSON.stringify({
imageId, // from step 1
storagePath, // from step 1
alt: "Trail Pack 38L, granite colorway", // required (a11y)
isPrimary: true,
}),
});Don't send a url — the service derives the public URL from storagePath
itself and rejects a client-supplied one (it was a spoofing vector). alt
is required.
Setting isPrimary: true atomically clears the primary flag on the item's
other images, so there's always exactly one hero. Collections use the same
flow against /merchant/collections/:id/images.
Managing images
await api(`/merchant/items/${itemId}/images`, { token }); // list
await api(`/merchant/items/${itemId}/images/${imageId}`, { // update meta
method: "PATCH", token,
body: JSON.stringify({ alt: "…", focalX: 0.5, focalY: 0.3, isPrimary: true }),
});
await api(`/merchant/items/${itemId}/images/order`, { // reorder
method: "PATCH", token, body: JSON.stringify({ imageIds: ["a", "b"] }),
});
await api(`/merchant/items/${itemId}/images/${imageId}`, { // delete
method: "DELETE", token,
});focalX / focalY (0–1) let a BYO frontend crop responsively around the
subject. Deleting an image removes the metadata row immediately; the storage
object is cleaned up by a background sweep.
Related
- Catalog: products & variants — what images attach to
- Collections & pages — collection cover images