The order's own artwork check, before the order. Reads the file (fetched server-side through the same SSRF-guarded, size-capped reader POST /v1/order uses; stores nothing unless auto_fix is sent) and grades it with the order's grader against sku: findings carries the order's codes (low_resolution, below_recommended_resolution, cmyk, no_transparency, touches_edge, no_bleed, content_in_bleed, no_cutline, unreachable, not_an_image, …), the bleed ones measured from the file's own pixels at the SKU's print size, effective_dpi and print_size_in are what the order measures, and would_refuse says whether a live POST /v1/order of this SKU with this file would be refused 422 artwork_not_printable. A test key gets the same verdict. Only when the file cannot be read just now (a timeout, a 5xx, a host the guard will not fetch) is the answer built from what you declared (width_px, height_px, dpi, the URL's extension), with verified: false and status: unverified — the order is not refused for an unread file either. Name the file as exactly one of file_url or asset_id, as an order line names it. Send neither sku nor product_type and the one read of the file is graded at a fixed set of sizes in each family (stickers, decals, magnets, posters), from a small size up to the largest our own plant prints (30 today, the bound on the list): fits holds, for each, the status, effective_dpi and would_refuse the call naming that sku answers, and largest_passing names, per family, the largest of those sizes whose status is passed. Without a sku, the top-level valid, status and would_refuse are measured at no size: read fits.
- Credential
- An API key in
x-api-key - Scopes
artwork:write
Parameters
Idempotency-Keyheader · string (uuid)Unique key for idempotent requests. If you retry a request with the same key within 24 hours, you receive the original response. The key is bound to the request payload: reusing it for a different payload returns 422
idempotency_key_reuserather than the original response, so a changed request is never silently dropped. The binding is over the JSON's content, not its bytes — keys in another order or other whitespace are the same request. Generate a new key for each new request. On an operation that moves money (an order, a batch, an item, a shipping or address change, a cancellation's refund), a key that cannot be checked because the idempotency ledger is unreachable is answered 503idempotency_unavailablewithRetry-After, and nothing runs or is charged: retry with the same key.
Request body application/json, required
file_urlstring (uri)URL of the artwork file to validate: an order line's
artwork_url. Send this orasset_id.asset_idstringAn asset you uploaded (POST /v1/assets), as an order line's
print_filenames it. Send this orfile_url; an id that is not yours is a 400, as on the order.matches ^ast_[A-Za-z0-9_-]+$
skustringThe SKU the file is for (e.g. STICKER-DIE-CUT-3X3): its print size and format are what the file is graded against, as the order grades its line. Without it,
product_typeis read as one.non-empty · at most 200 characters
sizestringThe line's free-text size (e.g. 4 x 2 in), read for a print size when the SKU names none, as on an order line.
non-empty · at most 100 characters
product_typestringProduct type for spec matching (e.g. 'sticker-die-cut', 'decal-vinyl', 'poster'), or a SKU. Read as the SKU when
skuis not sent.non-empty
width_pxintegerImage width in pixels. Read only when the file cannot be: a file that is read is measured.
at least 1
height_pxintegerImage height in pixels. Read only when the file cannot be.
at least 1
dpinumberImage resolution in dots per inch. Any positive number. A read file's resolution is measured at the SKU's print size; this is graded instead when the file cannot be read, or when neither the SKU nor
sizenames a print size (a product type such asposter), against that product's minimum.greater than 0
file_size_bytesintegerFile size in bytes
at least 1
auto_fixbooleanFix what the bleed findings name and store the result as a new asset on your account: a rectangle with
no_bleedgets its edge pixels extended past the trim by the product's bleed; a die-cut withcontent_in_bleedortouches_edgegets a wider transparent canvas, and one withno_cutline(separate shapes) a white border that joins them. The fixed file is graded again andauto_fixin the response says what changed and its verdict; order it by the new asset id. Your file is never changed. Needssku, a file that could be read whole, and theassets:writescope as well asartwork:write(a 403 insufficient_scope otherwise). Default false: nothing is stored.
Request
POST /api/v1/artwork/validate
curl -X POST "https://portal.printedsimply.com/api/v1/artwork/validate" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: validate-artwork-$(date +%s)" \
-d '{
"file_url": "https://example.com/logo.png",
"sku": "sticker-die-cut-3x3"
}'Response 200
Artwork validation result
data
validbooleanrequiredTrue when no finding refuses the file and no check fails: with a
sku, the order's verdict at its print size. Without asku(or aproduct_typethat is one), nothing is measured at a size (size_unknown), so it does not say the file prints at any: readfitsandlargest_passing;warningssays when no size infitsprints the file.file_urlstringrequiredproduct_typestring | nullAs you sent it.
resolved_product_typestring | nullWhat it resolved to — "sticker-die-cut-3x3" resolves to "sticker-die-cut" — so a normalization is visible rather than silent.
product_specsobject | nullproduct_specs.namestringproduct_specs.min_dpiintegerproduct_specs.max_sizestringproduct_specs.bleedstringproduct_specs.formatsstring[]checksobject[]requiredThe verdict as named checks. On a read file: file_format, resolution, color_space and, for a die-cut SKU, cut_line (with the margin measured around the design), or for another SKU whose bleed the file lets us measure, bleed_area, each from the order's findings (a fail is an error the order refuses for). On an unread file (
verified: false): the checks built from what you declared. file_size when you sentfile_size_bytes.checks[].namestringrequiredWhich check this is —
file_format,resolution,print_area,color,embedded_assets. The set differs by what was checked; treat it as an open vocabulary.checks[].statusstringrequiredfailblocks the print.warndoes not — it is advice that the artwork will print better if you act on it. Onlyfailshould stop a pipeline.One of
passfailwarnchecks[].messagestringrequiredThe finding in prose, safe to show a human.
warningsstring[]suggestionsstring[]skustring | nullThe SKU the file was graded against:
sku, elseproduct_type; null when neither was sent.asset_idstring | nullThe asset you named, when you named the file by
asset_id;file_urlis then its URL.verifiedbooleanTrue when the file was read and graded; false when it could not be read just now and the answer is from what you declared.
statusstringThe order's line status for this file: passed, warned, refused, or unverified (not read). Without a
skuit is graded at no print size, so resolution plays no part in it:fitsholds the status at each size.One of
passedwarnedrefusedunverifiedwould_refusebooleanWhether a live POST /v1/order of this SKU with this file would be refused 422 artwork_not_printable. Only a SKU our own plant makes is graded by the order, and an unread file is never refused. Without a
skuthere is no SKU to refuse, so it is false: each entry offitsanswers it for its own SKU.findingsobject[]The order's own findings for this file (ArtworkLineVerdict's), with its codes and severities.
findings[].codestringrequiredOne of
unreachablenot_an_imagelow_resolutionbelow_recommended_resolutioncmykno_transparencytouches_edgeno_bleedcontent_in_bleedno_cutlinesize_unknownvectorunverifiedformat_not_acceptednot_fetchabledesign_embeds_rastergenerated_designplacement_clippedgeometry_unverifiedfindings[].severitystringrequirederror refuses a live order; warning and info never do.
One of
errorwarninginfofindings[].messagestringrequiredwidth_pxinteger | nullMeasured from the file; null when it was not read or is vector.
height_pxinteger | nullMeasured from the file; null when it was not read or is vector.
print_size_innumber[] | nullThe print size resolution was measured at, in inches: the SKU's, else
size's. Null when neither names one.2 to 2 entries
effective_dpiinteger | nullPixels per inch with the file printed as large as the print size allows, as the order measures it.
min_dpiinteger | nullThe product's recommended minimum.
fitsobject[] | nullWhen neither
skunorproduct_typeis sent: the file, read once, graded at a fixed set of sizes in each family (30 today), stickers, decals (the wall decal), magnets and posters in that order, smallest first, each family's last the largest our own plant prints of it. Sizes between two of them are orderable and not graded. Each entry is what the call naming thatskuanswers. Null when askuorproduct_typewas sent, and when the file could not be read (verified: false), since nothing was measured.at most 30 entries
fits[].skustringrequiredThe size's SKU, as the price book spells it while our plant's own catalog is silent: a product GET /v1/products lists keeps the spelling listed there, and GET /v1/products/{sku} serves every one. Once the plant's catalog answers, its rows may spell or size products otherwise. The call naming this
skuanswers the same verdict.fits[].familystringrequiredThe SKU's family, as its catalog row's
familynames it.One of
stickersdecalsmagnetspostersfits[].sizestringrequiredThe SKU's print size, e.g. 3×3 in.
fits[].size_innumber[]requiredThe same size in inches, [width, height].
2 to 2 entries
fits[].statusstringrequiredThe order's line status for this file at this SKU: passed, warned or refused. Never unverified: an unread file has no
fits.One of
passedwarnedrefusedfits[].effective_dpiinteger | nullrequiredPixels per inch at this size, as the order measures it; null for a PDF or SVG, which is not measured in pixels, and for a file that is not an image.
fits[].would_refusebooleanrequiredWhether a live POST /v1/order of this SKU with this file would be refused 422 artwork_not_printable.
largest_passingobject[] | nullOne entry per family in
fits, in its order: the largest of that family's sizes infitswhosestatusispassed, the order's "prints well". Awarnedsize prints, with a warning, and is not counted. Null exactly whenfitsis.largest_passing[].familystringrequiredThe family, as
fits[].familynames it.One of
stickersdecalsmagnetsposterslargest_passing[].skustring | nullrequiredThe SKU of the family's largest passing size; null when no size of the family passes.
largest_passing[].sizestring | nullrequiredThat SKU's print size, e.g. 4×4 in; null when no size passes.
largest_passing[].size_innumber[] | nullrequiredThe same size in inches, [width, height]; null when no size passes.
2 to 2 entries
largest_passing[].effective_dpiinteger | nullrequiredThe file's resolution at that size; null when no size passes, and for a PDF or SVG.
auto_fixobject | anyWhat
auto_fix: truedid: null when it was not sent.auto_fix.appliedbooleanrequiredTrue when a fixed file was written and stored; false with
reasonwhen nothing was.auto_fix.reasonstringWhy nothing was fixed (nothing to fix, no sku, a vector file, a file not read whole, too large to decode). Only when
appliedis false.auto_fix.asset_idstringThe new asset (
source: fixup): order it as a line'sprint_file.auto_fix.urlstringThe new asset's URL.
auto_fix.content_typestringOne of
image/pngimage/jpegauto_fix.width_pxintegerauto_fix.height_pxintegerauto_fix.changesobject[]auto_fix.changes[].codestringrequiredThe finding this change answers.
One of
no_bleedcontent_in_bleedtouches_edgeno_cutlineauto_fix.changes[].descriptionstringrequiredWhat was done, in pixels and inches.
auto_fix.notesstring[]What the fix could not do, such as shapes too far apart to join with a border.
auto_fix.statusstringThe fixed file's own verdict for the same SKU.
One of
passedwarnedrefusedunverifiedauto_fix.would_refusebooleanWhether a live POST /v1/order of this SKU with the fixed file would be refused.
auto_fix.findingsobject[]The fixed file's findings, graded by the order's grader.
auto_fix.findings[].codestringrequiredOne of
unreachablenot_an_imagelow_resolutionbelow_recommended_resolutioncmykno_transparencytouches_edgeno_bleedcontent_in_bleedno_cutlinesize_unknownvectorunverifiedformat_not_acceptednot_fetchabledesign_embeds_rastergenerated_designplacement_clippedgeometry_unverifiedauto_fix.findings[].severitystringrequiredOne of
errorwarninginfoauto_fix.findings[].messagestringrequired
meta
pageintegertotalintegerlimitintegerThe page size actually applied, which is not necessarily the one asked for — the operation's own ceiling wins.
pagesintegertotaloverlimit, rounded up.next_cursorstring | nullOn a cursor-paged operation: send this back as
cursorfor the next page. Null on the last page. Absent on operations that do not page by cursor.has_morebooleanOn a list operation whose
metaisListMeta: whether another page follows this one.
{
"status": "success",
"data": {
"valid": true,
"file_url": "https://example.com/logo.png",
"product_type": null,
"resolved_product_type": "sticker-die-cut",
"product_specs": {
"name": "Die-Cut Sticker",
"min_dpi": 300,
"max_size": "17\" x 17\"",
"bleed": "0.125\"",
"formats": [
"png",
"pdf",
"svg"
]
},
"checks": [
{
"name": "file_format",
"status": "pass",
"message": "PNG, 1200×1200 px, read from the file."
},
{
"name": "resolution",
"status": "pass",
"message": "400 DPI at 3×3 in, at or above the 300 recommended."
},
{
"name": "color_space",
"status": "pass",
"message": "SRGB, the colours previews and mockups show."
},
{
"name": "cut_line",
"status": "pass",
"message": "Transparent background with a margin around the design: the cut follows its shape."
}
],
"warnings": [],
"suggestions": [
"Leave 0.125\" of bleed on every side; the file check cannot see it."
],
"sku": "sticker-die-cut-3x3",
"asset_id": null,
"verified": true,
"status": "passed",
"would_refuse": false,
"findings": [],
"width_px": 1200,
"height_px": 1200,
"print_size_in": [
3,
3
],
"effective_dpi": 400,
"min_dpi": 300,
"fits": null,
"largest_passing": null
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}Errors
Invalid request body or parameters. When the request failed the operation's schema,
detailscarriesformErrors(problems with the request as a whole),fieldErrors(messages keyed by top-level field) anderrors: one{ pointer, code, message }per refused value,pointeran RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) —/items/0/sku, with~written~0and/written~1, and""for the request as a whole. POST /v1/orders/bulk answerserrorsas one{ index, details }per refused order, eachdetailswith its own pointers from the request body (/orders/3/items/0/sku); POST /v1/orders/multi-ship keepserrorsas the{ formErrors, fieldErrors }it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top ofdetails, witherrorsbeside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it inmessageand carries noerrors.Missing, invalid or expired API key:
missing_api_key(none sent),invalid_api_key(unknown or revoked),key_expired(past the expiry set when it was created; create a new key).insufficient_scope —
auto_fix: trueon a key whose scopes leave outassets:write: the fixed file would be stored as an asset. Nothing was read or stored; validate withoutauto_fix, or grant the scope.payload_too_large — the request body exceeded this route's byte cap (256 KB unless the route raises it with a written reason). It is refused before it is parsed, so nothing was created. Send fewer rows, or use the endpoint's own asynchronous door where it has one.
Rate limit exceeded. Two windows can produce this status and
error.codesays which:rate_limit_exceededfor the per-minute sliding window,daily_quota_exceededfor the plan's daily quota. Both carryRetry-After; the daily one also carriesX-Daily-LimitandX-Daily-Remaining. The per-minute limits are plan-based — see Rate Limits above, or call GET /v1/me for this credential's own figures.