Printy3D

Working with the API

Uploads

Files go straight to object storage through presigned URLs; the API only sees their metadata. A simple upload takes one PUT; large files use a resumable multipart upload.

Two ways to upload#

Simple uploadResumable upload
EndpointPOST /v1/filesPOST /v1/files/resumable
Size per fileUp to 10 MiBAbove 10 MiB, up to your plan's limit (at most 1 GiB)
Bodyfilename, size_bytes, sha256filename, size_bytes, sha256, input_format
TransferOne presigned PUTOne presigned PUT per part (8 MiB parts), resumable
CompletionSynchronous: ready or rejectedAlways asynchronous: verifying, then poll
WhoEvery accountApproved accounts

Every account can upload files of up to 10 MiB, 100 MiB in total, with the simple upload. Once the Printy3D team has approved your account, you may upload larger files with the resumable upload, up to your plan's per-file limit (200 MB on the free plan, 2 GB on the developer and team plans, and never above 1 GiB) and within your plan's storage. Jobs run on those files without any further grant. Before approval the resumable upload answers 501 feature_unavailable.

Simple upload#

POST/v1/filesfiles:write
  1. Reserve the file with filename, its exact size_bytes and the hex sha256 of its bytes, and an Idempotency-Key. The answer is the File (awaiting_upload) and an upload: {url, method: "PUT", headers, expires_at}.
  2. PUT exactly those bytes to upload.url with upload.headers unchanged. The URL signs the length and a create-only If-None-Match: *, so it accepts the declared file once; it expires after 15 minutes. Do not send your API key to it.
  3. Complete it with POST /v1/files/{id}/complete. The API checks the size, the digest and the container and answers the File as ready or rejected. Send Prefer: respond-async to get 202 verifying at once and poll instead.

The quickstart shows the whole flow in curl, Python and TypeScript. If the PUT failed or the URL expired, replay the reserve request with the same Idempotency-Key (within 24 hours): you get the same file with a fresh URL.

Resumable upload#

The body adds input_format, which must match the file name's extension (zip for scan.zip). The answer's upload is an S3 multipart upload:

  • protocol: "s3_multipart", method: "PUT", part_size_bytes, expires_at (1 hour) and parts: one {part_number, offset, size_bytes, url} for each part the store does not hold yet.
  • PUT exactly size_bytes bytes, read from offset of your file, to each part's url, in any order or in parallel. Each URL signs its length and refuses any other. Part URLs are credentials: never log them.
  • To resume (after a failure, a restart or before the URLs expire), replay the same request with the same Idempotency-Key. The answer lists fresh URLs for the parts still missing; parts already sent are kept. Starting the multipart upload is limited to three times per file, failed attempts included; resuming does not count.
  • When parts comes back empty, complete the file. Completion assembles the parts (409 while any is missing) and always queues verification: 202 with status verifying. Poll GET /v1/files/{id} until ready or rejected. After completion a replay of the reserve request answers 409.

The samples use the api, storage and ok helpers from the quickstart.

import os, uuid, hashlib, time
import httpx

path = "scan.zip"
size = os.path.getsize(path)
digest = hashlib.sha256()
with open(path, "rb") as f:
    for chunk in iter(lambda: f.read(1 << 20), b""):
        digest.update(chunk)

request = {
    "url": "/v1/files/resumable",
    # Keep this key: replaying the same request with it resumes the upload.
    "headers": {"Idempotency-Key": str(uuid.uuid4())},
    "json": {"filename": "scan.zip", "input_format": "zip",
             "size_bytes": size, "sha256": digest.hexdigest()},
}
reserved = ok(api.post(**request))
file_id = reserved["file"]["id"]

for _ in range(3):
    parts = reserved["upload"]["parts"]          # only the parts still missing
    if not parts:
        break
    with open(path, "rb") as f:
        for part in parts:                       # any order, or in parallel
            f.seek(part["offset"])
            body = f.read(part["size_bytes"])
            try:
                storage.put(part["url"], content=body)   # never log part URLs
            except httpx.TransportError:
                pass                             # listed again by the next replay
    reserved = ok(api.post(**request))           # resume: fresh URLs for missing parts

file = ok(api.post(f"/v1/files/{file_id}/complete"))   # 202, status "verifying"
while file["status"] == "verifying":
    time.sleep(2)
    file = ok(api.get(f"/v1/files/{file_id}"))
print(file["status"], file.get("rejected_reason"))

File states#

statusMeaning
awaiting_uploadReserved; the bytes are not completed yet.
verifyingCompletion is queued; the API is checking the bytes.
readyVerified; jobs can use it.
rejectedFailed verification; rejected_reason says why (for example obj_materials_unsupported).

A File also reports its input_format and its kind: mesh, splat, pointcloud, medical, cad or archive. A PLY is classified by its header: zero faces with Gaussian-splat columns is a splat, zero faces without them a pointcloud. List your files with GET /v1/files.

File names#

filename is 5 to 200 characters with no /, \, : or control characters, and its extension names the format (Formats). An unknown extension, or .7z, is refused.

Retention#

Files and job outputs are kept for your plan's retention period (30 days on every plan today). After that the API answers 410 expired for the file, for new jobs on it and for downloads of its outputs, and leaves it out of listings.

Errors you may see#

StatuscodeWhen
413file_too_largeLarger than the upload lane or your plan allows.
422validation_failedBad file name or extension, size or digest format.
422idempotency_key_reusedThe same Idempotency-Key with a different body.
409conflictCompleting a multipart upload with parts missing, or replaying one that is complete.
429quota_exceededYour account's input storage is full (with Retry-After).
501feature_unavailableThe resumable upload before your account is approved.