Files
Artwork, engraving files and print data are attached to order items through a two-step upload. You first request an upload slot (POST /v1/uploads), then send the file content directly to the returned address with a single HTTP PUT, and finally reference the returned upload_id inside items[].files when you create the order. File content is validated when the order is accepted, not when the file is uploaded.
#Two-step upload
The file never passes through the Meykt application server. POST /v1/uploads only hands out a short-lived, signed storage address; the bytes go straight to storage.
This has three practical consequences:
- No request size or timeout problems. You are not limited by JSON body limits, and you never base64-encode a file into a request.
- Retrying is cheap. If the
PUTfails, repeat only thePUT. If the order call fails, repeat only the order call with the sameupload_idvalues. - Validation happens later. The upload slot accepts whatever you send. Type checking, SVG sanitizing and the move into permanent storage happen when
POST /v1/ordersaccepts the order that references the upload.
POST /v1/uploads requires the scope files:write. Creating the order requires orders:write. Scopes are not hierarchical — a key that only has orders:write cannot request upload slots.
#Step 1 request an upload slot
Send the file name you want the file to be stored under. The name is sanitized (directory parts removed, unusual characters replaced with _, truncated to 120 characters) and the sanitized value is echoed back as filename.
curl -X POST https://api.meykt.com/v1/uploads \
-H "Authorization: Bearer $MEYKT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "engraving-logo.svg"}'Response 201:
{
"data": {
"upload_id": "3f0a9c1e-2b77-4c1a-9f1c-0d2a4e6b8c10",
"filename": "engraving-logo.svg",
"upload_url": "https://…signed…",
"method": "PUT",
"expires_in_seconds": 7200,
"max_bytes": 52428800
}
}One upload_id belongs to exactly one file. Request a separate slot for every file you want to attach.
#Step 2 send the file
Send the raw file content as the request body of a single PUT to upload_url. No authorization header is needed — the address is already signed.
response=$(curl -s -X POST https://api.meykt.com/v1/uploads \
-H "Authorization: Bearer $MEYKT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "engraving-logo.svg"}')
upload_url=$(echo "$response" | jq -r '.data.upload_url')
upload_id=$(echo "$response" | jq -r '.data.upload_id')
curl -X PUT "$upload_url" --data-binary @engraving-logo.svgCheck the HTTP status of the PUT. Only a 2xx means the file arrived; on any other status the file is not stored and the upload_id will later be rejected with file_not_found.
#Step 3 reference the upload in an order
Add the upload_id to the files array of the item it belongs to. role is optional free text (max 40 characters) and is display information only.
curl -X POST https://api.meykt.com/v1/orders \
-H "Authorization: Bearer $MEYKT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_order_id": "ORDER-2026-1044",
"customer": { "name": "Anna Beispiel" },
"items": [{
"sku": "BECHER-GROSS",
"name": "Mug large",
"quantity": 2,
"personalization": { "engraving": "All the best" },
"files": [{ "upload_id": "3f0a9c1e-2b77-4c1a-9f1c-0d2a4e6b8c10", "role": "engraving" }]
}]
}'Files are accepted before anything is written. A single unusable file rejects the whole order with 422 and nothing is created — this is the same all-or-nothing rule that applies to field validation.
In responses, each item reports its files as name and role only:
{
"files": [{ "name": "engraving-logo.svg", "role": "engraving" }]
}The API does not return download URLs for order files. The files are visible on the order in the dashboard.
#Allowed file types
The actual content is checked, not the file extension and not the Content-Type you sent. Detection reads the first bytes of the stored file. Renaming a .mp4 to .svg does not help — it is rejected with file_type_not_allowed, and the error message names the type that was detected.
| Format | Detected type |
|---|---|
| PNG | image/png |
| JPEG | image/jpeg |
| WebP | image/webp |
| GIF | image/gif |
| SVG | image/svg+xml |
| TIFF | image/tiff |
| BMP | image/bmp |
| HEIC / HEIF / AVIF | image/heic, image/heif, image/avif |
application/pdf | |
| DXF | application/dxf |
| EPS / PS | application/postscript |
| ZIP (including ZIP-based Office formats) | application/zip |
| Plain text / CSV | text/plain, text/csv |
Notes:
- Adobe Illustrator files are accepted through the format they are actually written in: older PostScript-based
.aifiles are detected asapplication/postscript, current PDF-based.aifiles asapplication/pdf. - A file must start with a recognizable header. A truncated or empty upload is reported as
file_not_found, not as a type error. - SVG is sanitized automatically. Scripts, inline event handlers, external references and active URL schemes are stripped, and the file is stored with
Content-Disposition: attachment. The stored SVG can therefore differ from the bytes you uploaded.
#Limits
- 50 MB per file (
max_bytes: 52428800). - Up to 10 files per order item. More than 10 entries in
items[].filesis a schema violation and fails withvalidation_failed. - One file per
upload_id. upload_idmust be a UUID — the exact value returned byPOST /v1/uploads.
#Lifetime
- The
upload_urlis valid for 2 hours (expires_in_seconds: 7200). After that, request a new upload slot; the oldupload_idis unusable. - An upload that is never referenced by an order expires after 14 days. Nothing to clean up on your side, and there is no endpoint to delete an upload.
- Once an order referencing the upload is accepted, the file is stored permanently with that order and is no longer subject to the 14-day expiry.
#Reusing uploads on retry
Duplicate detection for orders is computed over the upload_id values, not over storage locations. That makes retries safe:
- Resending the identical order body, including the same
upload_idvalues, returns200with the existing order — never a duplicate. - Using new
upload_idvalues with the sameexternal_order_idcounts as different content and returns409order_already_exists_with_different_content. - If an order is rejected (file problem or validation error), the uploads stay usable. Fix the request and resend it with the same
upload_idvalues.
#Errors
Both file errors are reported with HTTP 422 and the top-level code validation_failed; the specific code sits in errors[] together with the exact position in field, using bracket notation.
{
"error": {
"code": "validation_failed",
"message": "The order was rejected because of file problems. Nothing was created.",
"doc_url": "https://www.meykt.com/en/developers/errors/validation_failed",
"errors": [
{
"code": "file_type_not_allowed",
"message": "The file for upload_id 3f0a9c1e-2b77-4c1a-9f1c-0d2a4e6b8c10 has an unsupported type (detected: video/mp4).",
"field": "items[0].files[0].upload_id"
}
]
}
}file_not_found — the upload_id is unknown, the PUT in step 2 never succeeded, or the upload already expired after 14 days. Fix: run POST /v1/uploads, PUT the content to upload_url, verify the PUT returned 2xx, then reference the upload_id. Details: /developers/errors/file_not_found.
file_type_not_allowed — the detected content is not in the allowed list. The message names the detected type. Fix: export the file in one of the formats listed under Allowed file types. Renaming the extension changes nothing, because the content is what is checked. Details: /developers/errors/file_type_not_allowed.
Both errors mean that no part of the order was created. Correct the file, then resend the complete order.
Full list of codes: /developers/errors. Order semantics, limits and warnings: /developers/orders.
Machine-readable version of this page: https://www.meykt.com/developers/files.md