Artwork and print files
Most custom print work needs a file. Around one line in six across the platform carries one, and for a made-to-order product the line is not really placeable without it: there is nothing to print.
This is a three-step flow, and the middle step does not touch this API at all.
1 POST /v1/orders/{orderNumber}/items/{itemNumber}/artwork/upload-url -> { uploadUrl, reservationName, expiresAt, ... }
2 PUT <uploadUrl> <- your file's bytes, straight to Collaterate's storage
3 POST /v1/orders/{orderNumber}/items/{itemNumber}/artwork { "reservationName": "..." } -> the attached fileBoth API calls need partner-api/orders:write - the same scope you already hold to submit an
order. Attaching the file is not a separate capability from placing the job; it is the step that
finishes it.
We never receive your file
Section titled “We never receive your file”Step 2 goes from you to storage. Nothing streams through this API, which is why there is no
request-size limit to work around and no 30-second ceiling on a large upload. It also means the
usual failure modes move: a rejected upload in step 2 comes back from a hostname that is not ours,
as a bare S3 error rather than a problem+json document.
expiresAt is about one minute away
Section titled “expiresAt is about one minute away”The upload URL is signed and short-lived. Call step 1 at the moment you are ready to upload, not in a batch ahead of time. A URL fetched now and used two minutes later is refused by storage.
If that happens, just call step 1 again. Nothing is wasted and nothing has to be cleaned up: a reservation that is never filled is simply never used.
expiresAt is read out of the URL’s own signature rather than assumed, so it is the real
deadline. Treat a null there as “one minute” and move on.
Treat uploadUrl as a credential
Section titled “Treat uploadUrl as a credential”Anyone holding it can write one object into Collaterate’s storage until it expires. Do not log it,
do not persist it, do not paste it into a bug report. The response carries Cache-Control: no-store for the same reason. If you need to show somebody what went wrong, quote the
requestId - every response on this API carries one.
contentType binds the upload, or does not
Section titled “contentType binds the upload, or does not”contentType on step 1 is optional, and the choice is real:
| You send | Your PUT must |
Response contentType |
|---|---|---|
"contentType": "application/pdf" |
carry exactly Content-Type: application/pdf |
"application/pdf" |
| nothing | carry whatever it likes | null |
When you send one it becomes part of the URL’s signature, so a mismatch is a 403 from storage
with no explanation attached. When you omit it the URL is unbound. Either way it is not recorded
against the file - it only affects the upload itself.
Worked example
Section titled “Worked example”import { PartnerApiClient } from '@smartpress/partner-api';
const client = new PartnerApiClient({ /* ... */ });
const slot = await client.orders.artworkUploadUrl(1000234, 884213, { fileName: 'flyer-front.pdf', contentType: 'application/pdf',});
// Not through the SDK: this goes to storage, not to the partner API.const upload = await fetch(slot.uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'application/pdf' }, body: bytes,});if (!upload.ok) throw new Error(`upload failed: ${upload.status}`);
const artwork = await client.orders.attachArtwork(1000234, 884213, { reservationName: slot.reservationName,});// artwork.fileSize and artwork.md5 are read back from storage: check them against what you sent.Files add up. They do not replace
Section titled “Files add up. They do not replace”Attaching a second file leaves the first in place, and that is deliberate rather than an oversight. Print jobs routinely need more than one: a two-sided piece is two artworks, a variable-data job carries its data file beside the art, and a revision goes up alongside what it supersedes so prepress can see both. About a third of all lines that carry artwork carry more than one file.
So repeat all three steps per file. There is no “replace the artwork” call, and sending a second one does not remove the first.
You cannot list or delete artwork through this API
Section titled “You cannot list or delete artwork through this API”Not an oversight, and worth knowing before you design around it.
Collaterate’s own listing and deletion of a line’s print files sit on an endpoint family that accepts only a storefront shopper’s browser session, and this API authenticates as a supplier-level service and cannot obtain such a session. Deletion is worse still: it additionally matches the file against the shopper who owns the underlying cart, so it would succeed or fail depending on internal state you cannot see.
This used to say the same limitation kept GET .../proof from returning a line’s proof files. It
does not: proof files come from the same place every other read on this API comes from, and that
endpoint has worked since 2026-08-19. Whether print files can be listed the same way is a separate,
unanswered question - they are a different record from proof files.
A route that answers 503 most of the time is worse than no route, so neither ships. That is also
why an attached file has no id in the response - there is no operation that would take one.
If you attach the wrong file, attach the right one - prepress sees both, and the newest is the one they work from - and tell your Collaterate contact so the wrong one can be retired.
When it goes wrong
Section titled “When it goes wrong”| What happened | You get |
|---|---|
| You attached before the upload finished | 409 artwork_reservation_not_usable |
The reservationName is unknown, or already attached |
409 artwork_reservation_not_usable |
| The line is cancelled | 409 order_item_cancelled |
(orderNumber, itemNumber) is not a line you can see |
404 order_item_not_found |
fileName is a path, too long, or missing |
400 invalid_request |
| The upload URL expired | a 403 from storage, not from us |
artwork_reservation_not_usable is the one to expect. Its detail carries Collaterate’s own
wording, which distinguishes the three causes. The fix is nearly always the same: confirm the
PUT returned 200, and if the URL had expired, request a fresh one and upload again.
Full list in Errors.
Reordering, proofs and what happens next
Section titled “Reordering, proofs and what happens next”Attaching a file puts the line in front of prepress and, on a line that is already on a placed order, flags it for a fresh look. If the product is configured with a proof, that is what eventually produces the proof you approve - see Tracking and line items.