Uploads artwork or a document as base64. Allowed types: image/png, image/jpeg, image/svg+xml, application/pdf. 50 MB cap. Returns the hosted URL to use in print files, proofs, or mockups.
- Credential
- An API key in
x-api-key - Scopes
assets: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
filenamestringrequirednon-empty · at most 255 characters
content_typestringrequiredOne of
image/pngimage/jpegimage/svg+xmlapplication/pdfdatastringrequiredBase64-encoded file contents. The 50 MB cap is on the decoded file; this bound is its base64 length (69,905,068 characters).
non-empty · at most 69905068 characters
Request
POST /api/v1/assets
curl -X POST "https://portal.printedsimply.com/api/v1/assets" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: upload-asset-$(date +%s)" \
-d '{
"filename": "logo.png",
"content_type": "image/png",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}'Response 200
Asset stored; includes the hosted url
data
idstringrequiredast_….POST /v1/orderaccepts this directly as an item'sprint_file, in place of a URL.filenamestringrequiredurlstringrequiredPublicly reachable URL for the stored bytes.
content_typestringrequiredOne of
image/pngimage/jpegimage/svg+xmlapplication/pdfsize_bytesintegerrequiredsourcestringrequiredWhere the bytes came from:
upload(sent through POST /v1/assets or the dashboard),mockup(a render from POST /v1/mockups/generate),listing(the photograph a checkout link gets on its first publish),design(artwork a design run generated),carryover(a listing image a listing import fetched and rehosted),manifest(a production batch's manifest),fixup(a copy of your artwork with its bleed, margin or outline fixed by POST /v1/artwork/validate with auto_fix). A fact, not a filter: every asset surface lists, exports and deletes a render exactly as an upload, and there is no?source=on the list. Files stored before the field existed were classified from their filenames and readuploadwhere nothing said otherwise.One of
uploadmockuplistingdesigncarryovermanifestfixupcreated_atstring (date-time)required
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": {
"id": "ast_bptSUfSLn2FInuMnC5S4fVD1",
"filename": "logo.png",
"url": "https://assets.example.com/assets/1792076423000-a8e85f4abc4c-logo.png",
"content_type": "image/png",
"size_bytes": 70,
"source": "upload",
"created_at": "2026-10-15T15:00:23.000Z"
},
"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).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.