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
| Format | Priced | Notes |
|---|---|---|
| STL | automatically | Binary or ASCII. Units are millimetres. |
| OBJ | automatically | Geometry only; materials and textures are ignored. |
| 3MF | automatically | Printable 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, STP | by an engineer | Send 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, andGET /v1/accountnames 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
completeanswersrejectedwithtoo_complex: simplify the mesh, or send it through contact us. - Count — your plan's
max_files, when set.
Upload in three steps
- Declare.
POST /v1/fileswithfilename(with its extension),size_bytesand the lowercase hexsha256of the exact bytes. The answer is201with the file inpendingand a one-timeuploadblock: aurl, theheadersto send and anexpires_at. - PUT the bytes to
upload.urlwith exactly the headers inupload.headers. Both are mandatory —Content-Typeandx-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. - Complete.
POST /v1/files/{id}/complete. We check the object's size againstsize_bytesand its hash againstsha256, read the model, sanitise a 3MF and write the canonical copy.statusbecomesreadyandfile.readyis 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.statusDeclare, 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:
| Code | What happened | Next step |
|---|---|---|
size_mismatch | The object's size differs from size_bytes. | Declare the exact size of the bytes you send, then upload again. |
hash_mismatch | The object's SHA-256 differs from sha256. | Hash the exact bytes you upload (lowercase hex), then upload again. |
upload_missing | No object arrived at the signed URL, or it expired. | Declare the file again and PUT within expires_at. |
not_a_model | The bytes hold no readable geometry for the declared extension. | Export the part again from your CAD or slicer as STL, OBJ or 3MF. |
too_complex | More geometry than we price automatically. | Simplify the mesh, or send the file through contact us. |
too_large | Over 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.
| What | How long |
|---|---|
| A test file no order uses | Deleted 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 uses | Kept until you delete it, within your plan's max_files when set. |
| A file used by an order | Kept 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 order | Deleted 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 order | Redacted 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.purgedfollows. - An open order, a pending quote or a running price job still needs it —
409 FILE_IN_USE, anderror.detailsnames 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.