Skip to content

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 file

Both 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.

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.

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.

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 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.

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.

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.

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.

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.