Skip to content
Lucent ImagingLucent Imaging icon
API reference

Uploads

Artwork goes straight from your server to our storage through a signed URL, so large files never pass through the API itself.

Create an upload URL

POST
/uploads
orders:write
60/min

PUT the bytes to uploadUrl with exactly the returned contentType, then reference url as a line's imageUrl. Content-Length is baked into the signature, so the declared size is the enforced size.

Request body

FieldTypeNotes
filename
required
string

Used for the extension and for the studio's own records. It does not become the stored object name.

1-1024 characters
contentType
required
string

One of image/jpeg, image/png, image/webp, image/tiff or image/heic. Baked into the signature, so the PUT must send exactly this.

1-255 characters
sizeBytes
required
integer

Exact byte length. Baked into the signature as Content-Length, so the declared size is the enforced size - a mismatch is rejected by storage, not by us.

max 524288000

Response

FieldTypeNotes
uploadUrl
required
string

PUT the bytes here. Short-lived - request it when you are ready to send.

contentType
required
string

Send exactly this as the PUT's Content-Type.

url
required
string

What you put on a quote line as imageUrl. This is the value to keep.

key
required
string

The underlying storage key. Useful in a support conversation; you do not need it to order.

Status codes

201

A signed upload target.

400

The request was rejected. Branch on error.code.

401

Missing, malformed, revoked, expired, or belonging to a disabled account - deliberately indistinguishable.

403

The key authenticated but lacks the required scope.

429

Rate limited. Honour Retry-After.

POST https://lucentimaging.com.au/api/print/v1/uploads

{
  "filename": "artwork.tif",
  "contentType": "image/tiff",
  "sizeBytes": 84213760
}

Accepted types are JPEG, PNG, WebP, TIFF and HEIC, up to 500 MB - a full-size giclée scan genuinely reaches that.

Sending the file

The signature covers both the content type and the content length you declared, so the PUT has to match what you asked for. If it does not, storage rejects it before we ever see it.

# 1. Ask for a signed target. The declared size is the ENFORCED size.
SIZE=$(wc -c < artwork.tif)
UPLOAD=$(curl -s "$LUCENT_API/uploads" \
  -H "Authorization: Bearer $LUCENT_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\":\"artwork.tif\",\"contentType\":\"image/tiff\",\"sizeBytes\":$SIZE}")

# 2. PUT the bytes with EXACTLY the content type you declared.
curl -s -X PUT "$(echo "$UPLOAD" | jq -r .data.uploadUrl)" \
  -H "Content-Type: image/tiff" \
  --upload-file artwork.tif

# 3. Keep this - it is the line's imageUrl.
IMAGE_URL=$(echo "$UPLOAD" | jq -r .data.url)

Artwork ownership

A quote line may only reference artwork uploaded by the account making the request. Anything else is refused with ARTWORK_NOT_ALLOWED at pricing time - before an order exists, not after.

This holds even for a URL that is otherwise valid. Object keys are unguessable, but unguessability is not an authorisation model: a key is not a secret, and a URL that leaks into a log or a support ticket must not become a way to print someone else's work.

The practical consequence is that you cannot pass through a URL you got from anywhere other than your own POST /uploads call - including one from a previous key on a different account. Upload it again.

Products that must print something are marked requiresArtwork in the catalogue. A line for one of those with no imageUrl is refused with ARTWORK_REQUIRED rather than quietly reaching the studio as a job with nothing to print.