Documentation
Partner API

Files

Upload STL, OBJ and 3MF in two calls and one PUT; what complete verifies; rejection codes; what we keep, for how long and how to delete.

Updated 2026-10-24MarkdownOpenAPI

A file is the 3D model an order item points at. You upload it once — two calls and one PUT — and reuse it across quotes and orders. Files belong to one environment and are visible to you alone.

Formats

FormatPricedNotes
STLautomaticallyBinary or ASCII. Units are millimetres.
OBJautomaticallyGeometry only; materials and textures are ignored.
3MFautomaticallyPrintable objects are read with the settings the file was saved with — material, nozzle, layer height, infill, supports. Embedded slicer scripts are stripped and facts.sanitised says so.
STEP, STPby an engineerSend them through contact us and we come back with a quote. POST /v1/files answers 422 for a .step or .stp filename and says the same.

Limits

  • Size — your plan's max_file_bytes: 64 MiB on the default plan; it can differ per plan, and GET /v1/account names yours.
  • Geometry — up to four million triangles for STL and OBJ; a 3MF is read within the same caps the public calculator uses. Beyond them complete answers rejected with too_complex: simplify the mesh, or send it through contact us.
  • Count — your plan's max_files, when set.

Upload in three steps

  1. Declare. POST /v1/files with filename (with its extension), size_bytes and the lowercase hex sha256 of the exact bytes. The answer is 201 with the file in pending and a one-time upload block: a url, the headers to send and an expires_at.
  2. PUT the bytes to upload.url with exactly the headers in upload.headers. Both are mandatory — Content-Type and x-goog-content-length-range — because the signed URL covers them; a PUT missing either is refused by the storage service. The body is the raw file, never multipart.
  3. Complete. POST /v1/files/{id}/complete. We check the object's size against size_bytes and its hash against sha256, read the model, sanitise a 3MF and write the canonical copy. status becomes ready and file.ready is emitted.
FILE=bracket-v3.stl
SIZE=$(stat -c %s "$FILE")                 # macOS: stat -f %z "$FILE"
HASH=$(sha256sum "$FILE" | cut -d' ' -f1)   # macOS: shasum -a 256 "$FILE" | cut -d' ' -f1

# 1. declare
DECLARED=$(curl -s -X POST "https://engine.layercrew.com/v1/files" \
  -H "Authorization: Bearer $LAYERCREW_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\":\"$FILE\",\"size_bytes\":$SIZE,\"sha256\":\"$HASH\"}")
FILE_ID=$(echo "$DECLARED" | jq -r .data.id)
UPLOAD_URL=$(echo "$DECLARED" | jq -r .data.upload.url)
RANGE=$(echo "$DECLARED" | jq -r '.data.upload.headers["x-goog-content-length-range"]')

# 2. PUT the raw bytes with both signed headers
curl -s -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/octet-stream" \
  -H "x-goog-content-length-range: $RANGE" \
  --data-binary @"$FILE"

# 3. complete
curl -s -X POST "https://engine.layercrew.com/v1/files/$FILE_ID/complete" \
  -H "Authorization: Bearer $LAYERCREW_API_KEY" | jq .data.status

Declare, PUT, complete — with the hash and size computed from the file

complete is idempotent: calling it on a ready file returns the file. While another completion of the same file is running it answers 409; wait a moment and call again. The upload slot expires after expires_at; declare the file again for a fresh one.

Rejections

When verification fails the file is rejected, complete answers 422 FILE_REJECTED, and rejection_code on the file says why:

CodeWhat happenedNext step
size_mismatchThe object's size differs from size_bytes.Declare the exact size of the bytes you send, then upload again.
hash_mismatchThe object's SHA-256 differs from sha256.Hash the exact bytes you upload (lowercase hex), then upload again.
upload_missingNo object arrived at the signed URL, or it expired.Declare the file again and PUT within expires_at.
not_a_modelThe bytes hold no readable geometry for the declared extension.Export the part again from your CAD or slicer as STL, OBJ or 3MF.
too_complexMore geometry than we price automatically.Simplify the mesh, or send the file through contact us.
too_largeOver your plan's max_file_bytes.Reduce the mesh, split the part, or ask us about a larger limit.

A .step or .stp filename is refused at POST /v1/files (422, field filename): STEP files are priced by an engineer — send them through contact us; STL, OBJ and 3MF are priced automatically here.

What we read from the file

facts carries what the parser saw: triangles, objects, the bounding box in millimetres (bbox_mm), the producer of a 3MF and whether it was sanitised. The geometry itself stays in our systems and is read by the slicer, by the engineer who reviews your order and by the printer that makes it.

What we keep, for how long, and how to delete

Your files are stored in the EU, encrypted at rest, under an object name no signed URL can target once the canonical copy is written. The retention rules are the plan's; the default plan's values are below, and GET /v1/account names yours under limits.

WhatHow long
A test file no order usesDeleted after test_file_retention_days; purge_after on the file says when. Set to null the first time an order uses it.
A live file no order usesKept until you delete it, within your plan's max_files when set.
A file used by an orderKept while any order needs it. Once its last order reaches a final state (delivered, cancelled, rejected), deleted after file_retention_days_after_last_order.
The production copy on an accepted orderDeleted together with the file. The order keeps the filename, the facts and the price, so your history stays complete.
Your customer's personal data on an orderRedacted after recipient_retention_days — 90 days on the default plan — or at once with DELETE /v1/orders/{id}/recipient on a finished order. See Orders.

To delete a file yourself, call DELETE /v1/files/{id}:

  • No order uses it — deleted now, 204.
  • Used only by finished orders — scheduled, deleted within the hour, 202; file.purged follows.
  • An open order, a pending quote or a running price job still needs it — 409 FILE_IN_USE, and error.details names it. Delete once that has finished.

Deleting a file removes its bytes and every production copy; purged_at is set and file.purged is emitted. The runbook that erases a partner account runs the same for every file. Every endpoint is in the reference.

We value your privacy

We use cookies for analytics and advertising to improve your experience and measure our marketing. See our Privacy Policy.