PUBLIC API · V1 · OPENAPI 3.1

Fabriken API

Price parts, place orders and follow them on the printer, from your code, your terminal or your AI agent. Everything here is the same API our own website uses.

Introduction

One REST API runs the whole factory: our website, the CLI, the MCP server and your code all use it. JSON over HTTPS, versioned under /v1. This page is generated from the contract, OpenAPI 3.1 (version 0.35.0), so it always matches what the API answers.

BASE URLapi.fabrikenlabs.se
VERSIONv1
FORMATJSON, UTF-8
THESE DOCSfabrikenlabs.se/docs/api

Authentication

Send your API key as a Bearer token. Live keys start with fab_live_ and can order. Keys starting with fab_test_ are reserved for sandbox keys, which will quote and order without printing or charging. A business account creates its keys, with their scopes and spending limits, under API keys and MCP in the app. Each key is shown once.

  • Keys belong to an account, not a person. Every key of the account sees its parts, quotes and orders.
  • Each key has its own spend limit, max per order and approval threshold. COMING
  • Revoke a key at any time. Requests with it fail at once with 401 unauthorized.
No key needed
GET /v1/price-listPOST /v1/pricesGET /v1/print-optionsGET /v1/catalogGET /v1/catalog/{sku}GET /v1/catalog/{sku}/modelGET /v1/capacityGET /v1/appearanceGET /v1/analyticsPOST /v1/contact
Key optional, for shop orders without an account
POST /v1/ordersGET /v1/orders/{order_id}POST /v1/orders/{order_id}/confirmPOST /v1/orders/previewGET /v1/support/chatPOST /v1/support/chat/messagesPUT /v1/support/chat/contextPOST /v1/support/chat/readGET /v1/support/chat/events
HEADER
Authorization: Bearer fab_live_••••8f2c

Money and units

All amounts are integers in öre (1 kr = 100 öre) in SEK. Fields end in _ore. Totals come both ex and incl VAT. Lengths are millimetres, weights grams, times minutes. Every price says which price_list_version it used.

Money and units
FIELDTYPEDESCRIPTION
total_ex_vat_oreintegerFor example, 74100 means 741.00 kr
price_list_versionstringThe version in force when the price was made
expires_atdate-time or nullQuotes lock the price for 24 hours
ROUNDING
price = start fee + quantity × (grams × rate + started minutes × hourly rate ÷ 60)
      rounded up to whole kronor, VAT on top

Create a quote

Slices your part on our servers with the same X2D profiles the printers use, so grams and minutes are exact. The price is locked for 24 hours.

Upload the STL file once, then quote it in any material. The quote answers at once as pending: ask again until it is ready or failed.

POST/v1/partsLIVEAPI KEY

Upload a part

Uploads one 3D model and runs the mesh check on it. Send the file itself as the request body (not multipart form data): STL (binary or ASCII, in millimetres) or 3MF. The largest file is the uploads setting's limit (200 MB, GET /v1/upload-limits). The body is streamed to disk as it arrives.

A 3MF file makes one part per printable object in its build (core spec and production extension, including Bambu Studio projects with 3D/Objects/*.model): components and transforms are applied, the model unit is honoured, plates and the arrangement in the file are ignored. Each part is stored as a binary STL and quoted, coloured and priced on its own. Colours and painting in the file are not used: the customer picks one colour per part, and source.colours_ignored says when the file had some. The answer is the first part, as for an STL, plus parts: every part made from the file, linked by source.upload_id. A 3MF is known by its content type (model/3mf), a .3mf file name or its zip signature. It is refused (422) when it cannot be read (unreadable_3mf), when it fails the zip bomb guards in the uploads setting (unsafe_archive), when it has nothing to print (no_mesh_objects) or more objects than one upload may make (too_many_objects).

The file is read and checked off the request thread, so other requests are not held up (contract 0.32.0). At most the uploads setting's check.max_at_once uploads are checked at once; more wait their turn, in order, and the answer comes when the check is done. Reading and checking one part (an STL, or one object of a 3MF) is stopped after check.time_limit_seconds: the upload is refused (422 mesh_check_timed_out, nothing is stored) with a message that names the object and asks for fewer triangles.

The part is stored even when the mesh check rejects it, so it can be priced by hand. mesh_check.issues says what is wrong in words a customer can read, and next_step says what to do: quote, fix_and_upload or request_price. A broken mesh (holes, edges shared by more than two triangles, triangles facing the wrong way) is rejected, not repaired, and points to Request price.

Parameters
FIELDTYPEDESCRIPTION
Idempotency-Keystring · header
required
A unique value per create request, for example a UUID. See Idempotency.
filenamestring · queryThe customer's file name, shown back to them. 1 to 255 characters.

Request body: the file itself, model/stl, required.

Response fields (11)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
filenamestring or null
required
The customer's file name, or for a part made from a 3MF file, the object's name.
formatstring
required
The stored model's format. A part made from a 3MF file is stored as STL too. Always stl.
size_bytesinteger
required
Size of the stored model.
sha256string
required
SHA-256 of the stored model (the uploaded STL, or the STL made from a 3MF object), hex.
created_atdate-time
required
mesh_checkMeshCheck
required
next_stepenum
required
What the customer can do with a part: quote: create a quote (the mesh check passed, perhaps with warnings, or a repaired model passed it). repair: the mesh check rejected it only for defects a repair can fix. Offer "Repair automatically (free)" (POST /v1/parts/{part_id}/repair) and "Manual repair by Fabriken" (a price request with reason: manual_repair, for the fee in GET /v1/price-list). fix_and_upload: fix the model as the issue messages say and upload it again. request_price: the automatic repair could not make it printable (offer the manual repair too), the slicer could not handle it, or the material has no instant price. Show "Request price" so Fabriken can price it by hand. One of quote, repair, fix_and_upload, request_price.
repairPartRepair or nullNull until a repair is asked for (automatic) or made (manual).
sourcePartSourceOnly on a part made from one object of a 3MF file. The parts of one upload share upload_id.
partsarray of Part
Errors (6)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
409idempotency_key_in_progressA request with the same idempotency key is still running. Code idempotency_key_in_progress. Retry shortly.
413uploadsfile_too_largeThe file is larger than the uploads setting allows. Code file_too_large.
415unsupported_media_typeThe content type is not a model's. Code unsupported_media_type.
422unreadable_3mfunsafe_archiveno_mesh_objectstoo_many_objectsmesh_check_timed_outuploadsidempotency_key_reusedA 3MF file we make no parts from: unreadable_3mf, unsafe_archive, no_mesh_objects or too_many_objects, with a message for the customer. mesh_check_timed_out (contract 0.32.0): one part took longer to read and check than the uploads setting's check.time_limit_seconds; the message names the 3MF object, if one was being read. Or idempotency_key_reused.
REQUEST
# Send the STL file itself as the body
curl "https://api.fabrikenlabs.se/v1/parts?filename=bracket-v3.stl" \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Idempotency-Key: 9b1d4e2a-6c3f-4a8e-b5d7-2f0c1e9a8b64" \
  -H "Content-Type: model/stl" \
  --data-binary @bracket-v3.stl
RESPONSE201 CREATED
{
  "id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
  "filename": "bracket-v3.stl",
  "format": "stl",
  "size_bytes": <integer>,
  "sha256": <string>,
  "created_at": "2026-11-06T17:40:58Z",
  "mesh_check": {
    "status": "ok",
    "repairable": false,
    "issues": [],
    "stats": <MeshStats>
  },
  "next_step": "quote",
  "repair": null
}
GET/v1/partsLIVEAPI KEY

List the account's parts

Every part the account has uploaded (contract 0.34.0), newest upload first, so a customer can price the next object of a 3MF file without uploading it again. The parts of one 3MF upload come together, in the file's order, sharing source.upload_id. Each part says what it needs next (next_step), how many paid orders it was in (order_count, cancelled and unpaid orders left out) and its newest price request.

Pages follow next_cursor until it is null. A page never splits the parts of one 3MF upload, so it can hold more parts than limit to finish the last upload on it. upload_id lists one upload's parts only, all on one page.

Parameters
FIELDTYPEDESCRIPTION
limitinteger · queryParts per page (the last upload on a page is finished even past it). 1 to 100. Default 50.
cursorstring · queryThe next_cursor of the page before. At most 200 characters.
upload_idstring · queryOnly the parts made from this 3MF upload (source.upload_id). At most 64 characters.
Response fields (2)
Response fields
FIELDTYPEDESCRIPTION
partsarray of ListedPart
required
next_cursorstring or null
required
Pass it as cursor for the next page. Null on the last page.
Errors (2)
Errors
STATUSCODEWHEN
400invalid_cursorAn unknown query field, or a cursor this list did not give (invalid_cursor).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
REQUEST
curl https://api.fabrikenlabs.se/v1/parts \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "parts": [
    <ListedPart>
  ],
  "next_cursor": <string or null>
}
GET/v1/parts/{part_id}LIVEAPI KEY

Get a part

Parameters
FIELDTYPEDESCRIPTION
part_idstring · path
required
Response fields (10)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
filenamestring or null
required
The customer's file name, or for a part made from a 3MF file, the object's name.
formatstring
required
The stored model's format. A part made from a 3MF file is stored as STL too. Always stl.
size_bytesinteger
required
Size of the stored model.
sha256string
required
SHA-256 of the stored model (the uploaded STL, or the STL made from a 3MF object), hex.
created_atdate-time
required
mesh_checkMeshCheck
required
next_stepenum
required
What the customer can do with a part: quote: create a quote (the mesh check passed, perhaps with warnings, or a repaired model passed it). repair: the mesh check rejected it only for defects a repair can fix. Offer "Repair automatically (free)" (POST /v1/parts/{part_id}/repair) and "Manual repair by Fabriken" (a price request with reason: manual_repair, for the fee in GET /v1/price-list). fix_and_upload: fix the model as the issue messages say and upload it again. request_price: the automatic repair could not make it printable (offer the manual repair too), the slicer could not handle it, or the material has no instant price. Show "Request price" so Fabriken can price it by hand. One of quote, repair, fix_and_upload, request_price.
repairPartRepair or nullNull until a repair is asked for (automatic) or made (manual).
sourcePartSourceOnly on a part made from one object of a 3MF file. The parts of one upload share upload_id.
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/parts/part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60 \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
  "filename": "bracket-v3.stl",
  "format": "stl",
  "size_bytes": <integer>,
  "sha256": <string>,
  "created_at": "2026-11-06T17:40:58Z",
  "mesh_check": {
    "status": "ok",
    "repairable": false,
    "issues": [],
    "stats": <MeshStats>
  },
  "next_step": "quote",
  "repair": null
}
GET/v1/parts/{part_id}/modelLIVEAPI KEY

A part's model, for the 3D view

The stored model as a binary or ASCII STL: the uploaded STL, or for a part made from a 3MF file, the STL made from its object. This is the file that is sliced and printed. The ETag is its SHA-256. For a repaired part it is the repaired model; version=original gives the model as uploaded.

Parameters
FIELDTYPEDESCRIPTION
part_idstring · path
required
versionenum · queryoriginal: the model as uploaded, before a repair. One of current, original. Default current.
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/parts/part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60/model \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
<binary>
POST/v1/parts/{part_id}/repairLIVEAPI KEY

Repair a part automatically

Free automatic repair of a part the mesh check rejected only for defects a repair can fix (mesh_check.repairable, next_step: repair). It welds duplicate corners and closes small gaps, removes zero-size, duplicate and stray triangles, splits edges shared by more than two triangles, fills holes up to the size limit, and turns every shell one way and outward. Then the mesh check runs again.

It runs in a worker off the request, with the time limit of the uploads setting (repair.time_limit_seconds). The answer is 202 with the part and repair.status: running; poll GET /v1/parts/{part_id} until it is repaired or failed.

Repaired: the part keeps its id, its model and mesh_check are the repaired ones (next_step: quote), and repair.original keeps the model as uploaded (GET /v1/parts/{part_id}/model?version=original). repair.summary says how many holes were closed and edges fixed. Failed (still rejected, or over the time limit): repair.failure says why, and next_step is request_price, for a manual repair by Fabriken or a price by hand.

Safe to repeat without an idempotency key: a part is repaired once, and asking again answers 200 with where its repair stands.

Parameters
FIELDTYPEDESCRIPTION
part_idstring · path
required
Response fields (10)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
filenamestring or null
required
The customer's file name, or for a part made from a 3MF file, the object's name.
formatstring
required
The stored model's format. A part made from a 3MF file is stored as STL too. Always stl.
size_bytesinteger
required
Size of the stored model.
sha256string
required
SHA-256 of the stored model (the uploaded STL, or the STL made from a 3MF object), hex.
created_atdate-time
required
mesh_checkMeshCheck
required
next_stepenum
required
What the customer can do with a part: quote: create a quote (the mesh check passed, perhaps with warnings, or a repaired model passed it). repair: the mesh check rejected it only for defects a repair can fix. Offer "Repair automatically (free)" (POST /v1/parts/{part_id}/repair) and "Manual repair by Fabriken" (a price request with reason: manual_repair, for the fee in GET /v1/price-list). fix_and_upload: fix the model as the issue messages say and upload it again. request_price: the automatic repair could not make it printable (offer the manual repair too), the slicer could not handle it, or the material has no instant price. Show "Request price" so Fabriken can price it by hand. One of quote, repair, fix_and_upload, request_price.
repairPartRepair or nullNull until a repair is asked for (automatic) or made (manual).
sourcePartSourceOnly on a part made from one object of a 3MF file. The parts of one upload share upload_id.
Errors (4)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
409repair_not_neededCode repair_not_needed, the part passed the mesh check.
422not_repairableCode not_repairable: the mesh check found a problem a repair cannot fix, for example a part too large or walls too thin. next_step says what to do.
REQUEST
# For a part with next_step repair. Free; poll the part until it is repaired
curl -X POST https://api.fabrikenlabs.se/v1/parts/part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60/repair \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
  "filename": "bracket-v3.stl",
  "format": "stl",
  "size_bytes": <integer>,
  "sha256": <string>,
  "created_at": "2026-11-06T17:40:58Z",
  "mesh_check": {
    "status": "rejected",
    "repairable": true,
    "issues": [
      {
        "code": "open_surface",
        "severity": "error",
        "message": "The model is not watertight: its surface has 2 holes.",
        "repairable": true,
        "holes": 2,
        "boundary_edges": 7
      }
    ],
    "stats": <MeshStats>
  },
  "next_step": "repair",
  "repair": {
    "method": "automatic",
    "status": "running",
    "requested_at": "2026-11-06T17:40:58Z",
    "finished_at": null,
    "duration_ms": null,
    "summary": null,
    "report": null,
    "failure": null,
    "original": null,
    "price_request_id": null
  }
}
POST/v1/quotesLIVEAPI KEY

Create a quote

Starts a quote for a part in one material and quantity. The answer comes at once with status: pending. The part is then sliced on the server with the same X2D profiles the printers use (ADR-0006), with automatic supports: the slicer adds tree supports only under overhangs that need them, and the price includes them. It is priced from the price list in force when the quote was created. Poll GET /v1/quotes/{quote_id} (see Location and Retry-After) until the status is ready or failed.

Only materials with an instant price in the price list in force are accepted; others answer 400 with code material_not_instant and next_step: request_price. A part whose mesh check rejected it cannot be quoted (422, code part_not_quotable).

layer_height_mm picks the slicer's process preset and infill_percent sets its sparse infill. Both change grams and machine time, so the price. They must be offered by the print options in force (GET /v1/print-options); anything else answers 400 with code print_option_not_offered and the field. Without them the quote gets the print options' defaults.

Parameters
FIELDTYPEDESCRIPTION
Idempotency-Keystring · header
required
A unique value per create request, for example a UUID. See Idempotency.
Request body
FIELDTYPEDESCRIPTION
part_idstring
required
1 to 64 characters.
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
quantityinteger
required
1 to 10000.
layer_height_mmnumberOne of the layer_heights in GET /v1/print-options, for example 0.12. Default: default_layer_height_mm. Above 0.
infill_percentintegerSparse infill in percent, within the infill range of GET /v1/print-options and on its steps. Default: infill.default_percent. 0 to 100.
Response fields (17)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
statusenum
required
pending: waiting for or being sliced, poll again. ready: priced and valid until expires_at. failed: no price, see failure. expired: was ready, but expires_at has passed. Create a new quote. One of pending, ready, failed, expired.
part_idstring
required
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
quantityinteger
required
layer_height_mmnumber or null
required
The layer height the part is sliced with. Null for quotes made before 7 November 2026 (sliced at 0.20 mm) and for prices set by hand.
infill_percentinteger or null
required
The sparse infill the part is sliced with, in percent. Null as above (the preset's own 15%, or set by hand).
created_atdate-time
required
ready_atdate-time or null
required
expires_atdate-time or null
required
24 hours after ready_at.
grams_per_unitnumber or null
required
Filament for one unit from the slicer, including supports and purge.
minutes_per_unitnumber or null
required
Printer time for one unit from the slicer, not rounded. The price charges per started minute. Sending grams_per_unit and minutes_per_unit to POST /v1/prices gives the same price.
pricePriceBreakdown or null
required
sliced_fileSlicedFile or null
required
failureQuoteFailure or null
required
pricingenum
required
instant: sliced and priced by the formula. manual: priced by hand for a price request (price_request_id); the sliced file was sliced by hand (slicer.name is manual) and an order charges exactly price.total_ex_vat_ore, with no volume tier and no test print. Only the totals of price are set. One of instant, manual.
price_request_idstring or null
required
Errors (6)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
409idempotency_key_in_progressA request with the same idempotency key is still running. Code idempotency_key_in_progress. Retry shortly.
422part_not_quotableidempotency_key_reusedCode part_not_quotable: the part's mesh check rejected it, see next_step. Or code idempotency_key_reused: the key was used for a different request.
503slicer_unavailableNo slicer is running on this server. Code slicer_unavailable. Try again later.
REQUEST
# Upload once, then quote
curl https://api.fabrikenlabs.se/v1/quotes \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Idempotency-Key: 9b1d4e2a-6c3f-4a8e-b5d7-2f0c1e9a8b64" \
  -H "Content-Type: application/json" \
  -d '{
    "part_id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
    "material": "PETG",
    "quantity": 10
  }'
RESPONSE202 ACCEPTED
{
  "id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80",
  "status": "pending",
  "part_id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
  "material": "PETG",
  "quantity": 10,
  "layer_height_mm": <number or null>,
  "infill_percent": <integer or null>,
  "created_at": "2026-11-06T17:41:12Z",
  "ready_at": null,
  "expires_at": null,
  "grams_per_unit": null,
  "minutes_per_unit": null,
  "price": null,
  "sliced_file": null,
  "failure": null,
  "pricing": <string>,
  "price_request_id": <string or null>
}
GET/v1/quotes/{quote_id}LIVEAPI KEY

Get a quote

Poll this until status is ready or failed. While pending, Retry-After says when to ask again.

Parameters
FIELDTYPEDESCRIPTION
quote_idstring · path
required
Response fields (17)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
statusenum
required
pending: waiting for or being sliced, poll again. ready: priced and valid until expires_at. failed: no price, see failure. expired: was ready, but expires_at has passed. Create a new quote. One of pending, ready, failed, expired.
part_idstring
required
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
quantityinteger
required
layer_height_mmnumber or null
required
The layer height the part is sliced with. Null for quotes made before 7 November 2026 (sliced at 0.20 mm) and for prices set by hand.
infill_percentinteger or null
required
The sparse infill the part is sliced with, in percent. Null as above (the preset's own 15%, or set by hand).
created_atdate-time
required
ready_atdate-time or null
required
expires_atdate-time or null
required
24 hours after ready_at.
grams_per_unitnumber or null
required
Filament for one unit from the slicer, including supports and purge.
minutes_per_unitnumber or null
required
Printer time for one unit from the slicer, not rounded. The price charges per started minute. Sending grams_per_unit and minutes_per_unit to POST /v1/prices gives the same price.
pricePriceBreakdown or null
required
sliced_fileSlicedFile or null
required
failureQuoteFailure or null
required
pricingenum
required
instant: sliced and priced by the formula. manual: priced by hand for a price request (price_request_id); the sliced file was sliced by hand (slicer.name is manual) and an order charges exactly price.total_ex_vat_ore, with no volume tier and no test print. Only the totals of price are set. One of instant, manual.
price_request_idstring or null
required
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/quotes/quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80 \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80",
  "status": "ready",
  "part_id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
  "material": "PETG",
  "quantity": 10,
  "layer_height_mm": <number or null>,
  "infill_percent": <integer or null>,
  "created_at": "2026-11-06T17:41:12Z",
  "ready_at": "2026-11-06T17:42:00Z",
  "expires_at": "2026-11-07T17:42:00Z",
  "grams_per_unit": 22.4,
  "minutes_per_unit": 96,
  "price": <PriceBreakdown>,
  "sliced_file": <SlicedFile>,
  "failure": null,
  "pricing": <string>,
  "price_request_id": <string or null>
}

The API did not answer when this page was built, so amounts show their type.

POST/v1/price-requestsLIVEAPI KEY

Request a price for a part we cannot quote instantly

Request price (ADR-0024). For a part the mesh check rejected (next_step: request_price or fix_and_upload), a material without an instant price in the price list in force (TPU, carbon fibre), or a quote that failed with next_step: request_price (send it as failed_quote_id). Fabriken gets an email, repairs and slices the part by hand, and answers with a price: a normal ready quote in this account (quote_id), which orders like any other. Or it declines with a message. The answer goes to email, and to the account's webhooks (price_request.answered, price_request.declined).

A part and material that get an instant price answer 422 instant_price_available: create a quote instead.

Manual repair (contract 0.31.0): with reason: manual_repair the customer chose "Manual repair by Fabriken" for a part rejected only for defects a repair can fix (mesh_check.repairable, also after a failed automatic repair). The fee of the cart setting in force (manual_repair_fee in GET /v1/price-list) is kept on the request as fee. Fabriken uploads a repaired STL (PUT /v1/price-requests/{price_request_id}/repaired-model), which is quoted automatically and answers the request, or answers with a price as usual. The first paid order with the part carries the fee as its own line (kind: fee). A declined request costs nothing. A part a repair could not help answers 422 manual_repair_not_possible.

Parameters
FIELDTYPEDESCRIPTION
Idempotency-Keystring · header
required
A unique value per create request, for example a UUID. See Idempotency.
Request body
FIELDTYPEDESCRIPTION
part_idstring
required
At most 64 characters.
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
quantityinteger
required
1 to 10000.
emailstring
required
Where the answer goes. At most 254 characters.
notestringAnything Fabriken should know, for example the use or a deadline. At most 2000 characters.
failed_quote_idstringA quote of this part and material that failed with next_step: request_price. At most 64 characters.
reasonenumThe customer chose a manual repair by Fabriken, for the fee. Leave it out otherwise: the reason is worked out from the part. One of manual_repair.
Response fields (20)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
statusenum
required
One of open, answered, declined.
reasonenum
required
Why there is no instant price. manual_repair, the customer chose a repair by Fabriken. One of mesh_rejected, material_not_instant, slicing_failed, manual_repair.
part_idstring
required
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
quantityinteger
required
notestring or null
required
emailstring
required
failed_quote_idstring or null
required
created_atdate-time
required
sliced_fileobject or null
required
The file Fabriken sliced by hand, once uploaded.
quote_idstring or null
required
The ready quote made from the answer. Order it like any quote.
responsestring or null
required
Fabriken's message with the answer or the decline.
closed_atdate-time or null
required
feeobject or null
required
The manual repair fee as agreed when asked (manual_repair only), ex VAT, and incl VAT at the cart setting's VAT rate. Charged as its own line on the first paid order with the part, never if declined.
repaired_modelobject or null
required
The repaired STL Fabriken uploaded, with the mesh check it passed.
pending_quote_idstring or null
required
The quote being made from the repaired model. Once ready it answers the request (quote_id); if it fails, Fabriken answers by hand.
accountobject or null
required
The account that sent the request, with its name, for the ops list (contract 0.25.0). Null only if the account is gone.
partPart or null
required
The part with its mesh check, as GET /v1/parts/{part_id} gives it, so Fabriken staff see the file name and the findings without reading the account's parts (contract 0.25.0). Null only if the part is gone.
quoteQuote or null
required
The quote made from the answer, with its price and expires_at (status expired once that has passed). Null until answered (contract 0.25.0).
Errors (5)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
409idempotency_key_in_progressA request with the same idempotency key is still running. Code idempotency_key_in_progress. Retry shortly.
422instant_price_availablequote_not_failedidempotency_key_reusedCode instant_price_available (with next_step: quote): this part and material get an instant price. Code quote_not_failed: failed_quote_id is not a quote of this part and material that failed with next_step: request_price. Or idempotency_key_reused.
REQUEST
curl https://api.fabrikenlabs.se/v1/price-requests \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Idempotency-Key: 9b1d4e2a-6c3f-4a8e-b5d7-2f0c1e9a8b64" \
  -H "Content-Type: application/json" \
  -d '{
    "part_id": <string>,
    "material": <string>,
    "quantity": <integer>,
    "email": <string>
  }'
RESPONSE201 CREATED
{
  "id": <string>,
  "status": <string>,
  "reason": <string>,
  "part_id": <string>,
  "material": <string>,
  "quantity": <integer>,
  "note": <string or null>,
  "email": <string>,
  "failed_quote_id": <string or null>,
  "created_at": <date-time>,
  "sliced_file": <object or null>,
  "quote_id": <string or null>,
  "response": <string or null>,
  "closed_at": <date-time or null>,
  "fee": <object or null>,
  "repaired_model": <object or null>,
  "pending_quote_id": <string or null>,
  "account": <object or null>,
  "part": <Part>,
  "quote": <Quote>
}
GET/v1/price-requestsLIVEAPI KEY

List price requests

The account's price requests, newest first. Fabriken staff see every account's (the ops list).

Parameters
FIELDTYPEDESCRIPTION
statusenum · queryOne of open, answered, declined.
Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
price_requestsarray of RequestPrice
required
Errors (1)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
REQUEST
curl https://api.fabrikenlabs.se/v1/price-requests \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "price_requests": [
    <RequestPrice>
  ]
}
GET/v1/price-requests/{price_request_id}LIVEAPI KEY

Get a price request

Another account's request answers 404. Fabriken staff read any.

Parameters
FIELDTYPEDESCRIPTION
price_request_idstring · path
required
Response fields (20)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
statusenum
required
One of open, answered, declined.
reasonenum
required
Why there is no instant price. manual_repair, the customer chose a repair by Fabriken. One of mesh_rejected, material_not_instant, slicing_failed, manual_repair.
part_idstring
required
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
quantityinteger
required
notestring or null
required
emailstring
required
failed_quote_idstring or null
required
created_atdate-time
required
sliced_fileobject or null
required
The file Fabriken sliced by hand, once uploaded.
quote_idstring or null
required
The ready quote made from the answer. Order it like any quote.
responsestring or null
required
Fabriken's message with the answer or the decline.
closed_atdate-time or null
required
feeobject or null
required
The manual repair fee as agreed when asked (manual_repair only), ex VAT, and incl VAT at the cart setting's VAT rate. Charged as its own line on the first paid order with the part, never if declined.
repaired_modelobject or null
required
The repaired STL Fabriken uploaded, with the mesh check it passed.
pending_quote_idstring or null
required
The quote being made from the repaired model. Once ready it answers the request (quote_id); if it fails, Fabriken answers by hand.
accountobject or null
required
The account that sent the request, with its name, for the ops list (contract 0.25.0). Null only if the account is gone.
partPart or null
required
The part with its mesh check, as GET /v1/parts/{part_id} gives it, so Fabriken staff see the file name and the findings without reading the account's parts (contract 0.25.0). Null only if the part is gone.
quoteQuote or null
required
The quote made from the answer, with its price and expires_at (status expired once that has passed). Null until answered (contract 0.25.0).
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/price-requests/{price_request_id} \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": <string>,
  "status": <string>,
  "reason": <string>,
  "part_id": <string>,
  "material": <string>,
  "quantity": <integer>,
  "note": <string or null>,
  "email": <string>,
  "failed_quote_id": <string or null>,
  "created_at": <date-time>,
  "sliced_file": <object or null>,
  "quote_id": <string or null>,
  "response": <string or null>,
  "closed_at": <date-time or null>,
  "fee": <object or null>,
  "repaired_model": <object or null>,
  "pending_quote_id": <string or null>,
  "account": <object or null>,
  "part": <Part>,
  "quote": <Quote>
}

Place an order

Orders ready quotes. Always send an Idempotency-Key: if the network drops and you retry, the order is placed only once.

The order starts as awaiting_payment with a checkout to show the buyer. When the checkout is complete, confirm the order: it is paid, and one print job is made per printed unit.

Idempotency

A unique value per create request, for example a UUID (ADR-0007). Sending the same key with the same request again returns the same part or quote (same id, same status code, in its current state) with Idempotent-Replayed: true, and creates nothing new. The same key with a different request is refused with 422. A key is kept for 24 hours, separately for each API key and endpoint, so two API keys never share idempotency keys. A request that fails is not kept, so it can be retried with the same key. A request that never finished, because the server stopped during it, frees its key after 5 minutes.

SAME KEY, SAME REQUEST AGAIN
Idempotent-Replayed: true

true when this answer is a replay of an earlier request with the same idempotency key.

Spend limits and approvalsCOMING

Each key gets its own spend limit, maximum per order and approval threshold. If an order is above the key's approval threshold, it is created with status approval_required and waits for a human in the app.

ABOVE THE APPROVAL THRESHOLDCOMING
{
  "id": "ord_1042c7e9b3a54d6f8e2a0b1c4d5e6f70",
  "number": "FAB-1043",
  "status": "approval_required",
  "approval": {
    "required": true,
    "reason": "above_key_threshold",
    "threshold_ex_vat_ore": 30000,
    "approve_url": "https://fabrikenlabs.se/app/approve/FAB-1043"
  }
}

The design's proposal; the fields may change. 30000 is an example threshold: each key sets its own.

POST/v1/ordersLIVEKEY OPTIONAL

Create an order and start its checkout

One kind of order for the app and the shop (ADR-0017). Lines are either quotes (quote_id, needs the API key of the quote's account) or shop products (sku, material, colour, quantity, and name for products that need one). Shop orders need no key.

The order starts as awaiting_payment with a Kustom Checkout (ADR-0008): render checkout.html_snippet in the page. Kustom collects email, address, shipping choice and payment. When Kustom sends the customer to the confirmation page, call POST /v1/orders/{order_id}/confirm.

Without an API key the Idempotency-Key must be at least 20 characters, for example a UUID.

Parameters
FIELDTYPEDESCRIPTION
Idempotency-Keystring · header
required
A unique value per create request, for example a UUID. See Idempotency.
Request body
FIELDTYPEDESCRIPTION
linesarray of objects
required
1 to 50 items.
lines[].quote_idstring
required
with quote_id
lines[].colourstring
with quote_id
A colour id from GET /v1/print-options whose materials include the quote's material. Without it, any colour of the material.
lines[].test_printboolean
with quote_id
One unit at the test print discount (cart setting). Only with quantity 1, once per part per account; the same file counts as the same part (ADR-0016).
lines[].skustring
required
with sku
lines[].materialenum
required
with sku
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
lines[].colourstring
required
with sku
Colour id from the catalog.
lines[].quantityinteger
required
with sku
1 to 100.
lines[].namestring
with sku
For products with personalisation required.
discount_codestringA discount code as the customer typed it. A code that does not apply answers 422 discount_code_rejected with the reason; check it first with POST /v1/orders/preview. 1 to 64 characters.
localeenumThe language the order is placed in (contract 0.28.0, ADR-0025): the website's, sv on the Swedish site and en under /en. Kustom's checkout opens in it (sv-SE or en-SE), our shipping option texts and Fraktjakt's service names and delivery times are in it, and later emails can use it. Carrier and pickup point names stay as they are. Default sv. One of sv, en.
Response fields (17)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
numberstring
required
Short number for people, for example FAB-1042.
statusenum
required
One of awaiting_payment, queued, printing, qc, packing, shipped, delivered, cancelled.
created_atdate-time
required
linesarray of OrderLine
required
customerobject or nullFrom Kustom once paid.
delivery_addressAddress or null
shippingobject or null
totalsobject
required
Without shipping while awaiting_payment; with the chosen shipping once paid.
paymentobject
required
checkoutobject or nullOnly while awaiting_payment.
follow_tokenstring or nullOnly in the first answer to POST /v1/orders for an order made without an API key: the secret token that lets the customer see the order (?token=). Put it in the order page link. Keep it, it is never shown again.
discount_codestring or nullThe discount code used, normalised.
localeenumThe language the order is placed in (contract 0.28.0, ADR-0025): the website's, sv on the Swedish site and en under /en. Kustom's checkout opens in it (sv-SE or en-SE), our shipping option texts and Fraktjakt's service names and delivery times are in it, and later emails can use it. Carrier and pickup point names stay as they are. Default sv. One of sv, en.
shipmentobject or nullThe booked shipment, once the pack station has booked it.
settings_versionsobjectThe settings versions the order was priced with, by kind, for example price_list 2026-11-07 and catalog seed-2026-11-06.
historyarray of objects
required
When each status was reached, for the timeline.
Errors (8)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredspend_limit_exceededapproval_requiredThe caller may not order for this account (forbidden, account_required), or the API key's rules refuse the order (ADR-0007): spend_limit_exceeded (above its max per order, or past its monthly limit this calendar month) or approval_required (above its approval threshold; approvals by email come later). Amounts are ex VAT before shipping. Nothing is created.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
409idempotency_key_in_progressA request with the same idempotency key is still running. Code idempotency_key_in_progress. Retry shortly.
422quote_not_readyquote_expiredproduct_not_availablecolour_not_availablename_requiredname_not_allowedname_too_longquote_needs_api_keyidempotency_key_reusedA line cannot be ordered. Codes: quote_not_ready, quote_expired, product_not_available (unknown product, or no file for that material), colour_not_available (no such colour id, or not in the line's material), name_required, name_not_allowed, name_too_long, quote_needs_api_key, or idempotency_key_reused.
502payment_failedKustom refused the request. Code payment_failed; the message says what Kustom answered.
503checkout_unavailableshipping_unavailablepayment_unavailableNo payment provider is configured on this server (checkout_unavailable), shipping could not be priced (shipping_unavailable), or Kustom could not be reached (payment_unavailable). Nothing is charged. Shipping prices are reused for 10 minutes and, when the carrier cannot answer, a quote up to a day old for the same parcels is used, so shipping_unavailable means no quote was at hand: show the message and let the customer try again after retry_after_seconds (also the Retry-After header).
REQUEST
curl https://api.fabrikenlabs.se/v1/orders \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Idempotency-Key: 9b1d4e2a-6c3f-4a8e-b5d7-2f0c1e9a8b64" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "quote_id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80"
      }
    ]
  }'
RESPONSE201 CREATED
{
  "id": "ord_1042c7e9b3a54d6f8e2a0b1c4d5e6f70",
  "number": "FAB-1042",
  "status": "awaiting_payment",
  "created_at": "2026-11-06T17:45:03Z",
  "lines": [
    {
      "id": "line_3a7d2e9c4b1f4a6e8d0c2b5a7e9f1d30",
      "kind": "quote",
      "quote_id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80",
      "part_id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
      "filename": "bracket-v3.stl",
      "material": "PETG",
      "quantity": 10,
      "unit_price_ex_vat_ore": <integer>,
      "discount_ex_vat_ore": <integer>,
      "total_ex_vat_ore": <integer>
    }
  ],
  "totals": {
    "items_ex_vat_ore": <integer>,
    "discount_ex_vat_ore": <integer>,
    "shipping_ex_vat_ore": <integer>,
    "total_ex_vat_ore": <integer>,
    "vat_ore": <integer>,
    "total_inc_vat_ore": <integer>
  },
  "payment": {
    "provider": <string>,
    "mode": <string>,
    "status": "pending"
  },
  "checkout": {
    "html_snippet": <string>
  },
  "history": [
    {
      "status": "awaiting_payment",
      "at": "2026-11-06T17:45:03Z"
    }
  ]
}

Amounts depend on the discounts in force when you order, so they show their type here.

GET/v1/ordersLIVEAPI KEY

List orders

The account's orders, newest first. Fabriken staff see every order. Filter with status, for example status=queued,printing.

Parameters
FIELDTYPEDESCRIPTION
statusstring · queryComma-separated order statuses.
Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
ordersarray of Order
required
Errors (3)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
REQUEST
curl "https://api.fabrikenlabs.se/v1/orders?status=queued,printing" \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "orders": [
    <Order>
  ]
}
GET/v1/orders/{order_id}LIVEKEY OPTIONAL

Get an order

With a key or session of the order's account, as Fabriken staff, or with the order's follow_token or the signed link from the order email (?token=) for orders placed without an account.

Parameters
FIELDTYPEDESCRIPTION
order_idstring · path
required
tokenstring · queryThe order's follow_token, for orders made without an API key.
Response fields (17)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
numberstring
required
Short number for people, for example FAB-1042.
statusenum
required
One of awaiting_payment, queued, printing, qc, packing, shipped, delivered, cancelled.
created_atdate-time
required
linesarray of OrderLine
required
customerobject or nullFrom Kustom once paid.
delivery_addressAddress or null
shippingobject or null
totalsobject
required
Without shipping while awaiting_payment; with the chosen shipping once paid.
paymentobject
required
checkoutobject or nullOnly while awaiting_payment.
follow_tokenstring or nullOnly in the first answer to POST /v1/orders for an order made without an API key: the secret token that lets the customer see the order (?token=). Put it in the order page link. Keep it, it is never shown again.
discount_codestring or nullThe discount code used, normalised.
localeenumThe language the order is placed in (contract 0.28.0, ADR-0025): the website's, sv on the Swedish site and en under /en. Kustom's checkout opens in it (sv-SE or en-SE), our shipping option texts and Fraktjakt's service names and delivery times are in it, and later emails can use it. Carrier and pickup point names stay as they are. Default sv. One of sv, en.
shipmentobject or nullThe booked shipment, once the pack station has booked it.
settings_versionsobjectThe settings versions the order was priced with, by kind, for example price_list 2026-11-07 and catalog seed-2026-11-06.
historyarray of objects
required
When each status was reached, for the timeline.
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/orders/ord_1042c7e9b3a54d6f8e2a0b1c4d5e6f70 \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": "ord_1042c7e9b3a54d6f8e2a0b1c4d5e6f70",
  "number": "FAB-1042",
  "status": "queued",
  "created_at": "2026-11-06T17:45:03Z",
  "lines": [
    {
      "id": "line_3a7d2e9c4b1f4a6e8d0c2b5a7e9f1d30",
      "kind": "quote",
      "quote_id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80",
      "part_id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
      "filename": "bracket-v3.stl",
      "material": "PETG",
      "quantity": 10,
      "unit_price_ex_vat_ore": <integer>,
      "discount_ex_vat_ore": <integer>,
      "total_ex_vat_ore": <integer>
    }
  ],
  "totals": {
    "items_ex_vat_ore": <integer>,
    "discount_ex_vat_ore": <integer>,
    "shipping_ex_vat_ore": <integer>,
    "total_ex_vat_ore": <integer>,
    "vat_ore": <integer>,
    "total_inc_vat_ore": <integer>
  },
  "payment": {
    "provider": <string>,
    "mode": <string>,
    "status": "payment_authorized"
  },
  "checkout": null,
  "history": [
    {
      "status": "awaiting_payment",
      "at": "2026-11-06T17:45:03Z"
    },
    {
      "status": "queued",
      "at": "2026-11-06T17:47:30Z"
    }
  ]
}

Amounts depend on the discounts in force when you order, so they show their type here.

POST/v1/orders/{order_id}/confirmLIVEKEY OPTIONAL

Confirm an order after Kustom Checkout

Call it from the confirmation page Kustom sends the customer to. The API asks Kustom for the result: when the purchase is complete, the order is paid (payment_authorized), stock is taken and print jobs are created; the price Kustom shows is checked against our own. Safe to call more than once. Kustom's push to the API does the same.

Parameters
FIELDTYPEDESCRIPTION
order_idstring · path
required
tokenstring · queryThe order's follow_token, for orders made without an API key.
Response fields (17)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
numberstring
required
Short number for people, for example FAB-1042.
statusenum
required
One of awaiting_payment, queued, printing, qc, packing, shipped, delivered, cancelled.
created_atdate-time
required
linesarray of OrderLine
required
customerobject or nullFrom Kustom once paid.
delivery_addressAddress or null
shippingobject or null
totalsobject
required
Without shipping while awaiting_payment; with the chosen shipping once paid.
paymentobject
required
checkoutobject or nullOnly while awaiting_payment.
follow_tokenstring or nullOnly in the first answer to POST /v1/orders for an order made without an API key: the secret token that lets the customer see the order (?token=). Put it in the order page link. Keep it, it is never shown again.
discount_codestring or nullThe discount code used, normalised.
localeenumThe language the order is placed in (contract 0.28.0, ADR-0025): the website's, sv on the Swedish site and en under /en. Kustom's checkout opens in it (sv-SE or en-SE), our shipping option texts and Fraktjakt's service names and delivery times are in it, and later emails can use it. Carrier and pickup point names stay as they are. Default sv. One of sv, en.
shipmentobject or nullThe booked shipment, once the pack station has booked it.
settings_versionsobjectThe settings versions the order was priced with, by kind, for example price_list 2026-11-07 and catalog seed-2026-11-06.
historyarray of objects
required
When each status was reached, for the timeline.
Errors (5)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
409amount_mismatchKustom's total does not match the order. Code amount_mismatch. The purchase is cancelled at Kustom.
502payment_failedKustom refused the request. Code payment_failed; the message says what Kustom answered.
503payment_unavailableKustom could not be reached or had an error (payment_unavailable), or the order's Kustom keys are no longer set. Try again later.
REQUEST
curl -X POST https://api.fabrikenlabs.se/v1/orders/ord_1042c7e9b3a54d6f8e2a0b1c4d5e6f70/confirm \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": "ord_1042c7e9b3a54d6f8e2a0b1c4d5e6f70",
  "number": "FAB-1042",
  "status": "queued",
  "created_at": "2026-11-06T17:45:03Z",
  "lines": [
    {
      "id": "line_3a7d2e9c4b1f4a6e8d0c2b5a7e9f1d30",
      "kind": "quote",
      "quote_id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80",
      "part_id": "part_5c2e9a41d07b4f3e8a6c1b2d3e4f5a60",
      "filename": "bracket-v3.stl",
      "material": "PETG",
      "quantity": 10,
      "unit_price_ex_vat_ore": <integer>,
      "discount_ex_vat_ore": <integer>,
      "total_ex_vat_ore": <integer>
    }
  ],
  "totals": {
    "items_ex_vat_ore": <integer>,
    "discount_ex_vat_ore": <integer>,
    "shipping_ex_vat_ore": <integer>,
    "total_ex_vat_ore": <integer>,
    "vat_ore": <integer>,
    "total_inc_vat_ore": <integer>
  },
  "payment": {
    "provider": <string>,
    "mode": <string>,
    "status": "payment_authorized"
  },
  "checkout": null,
  "history": [
    {
      "status": "awaiting_payment",
      "at": "2026-11-06T17:45:03Z"
    },
    {
      "status": "queued",
      "at": "2026-11-06T17:47:30Z"
    }
  ]
}

Amounts depend on the discounts in force when you order, so they show their type here.

POST/v1/orders/previewLIVEKEY OPTIONAL

Price a cart before ordering

The same lines and discount code as POST /v1/orders, priced with the settings in force (ADR-0016, ADR-0018), without creating anything: each line with its discounts, the upsell offers ("add one more and get 10% off"), whether the code applies and why not, totals without shipping, and the free shipping threshold. Shipping is chosen in Kustom's checkout. No key needed for shop lines.

Request body
FIELDTYPEDESCRIPTION
linesarray of objects
required
1 to 50 items.
lines[].quote_idstring
required
with quote_id
lines[].colourstring
with quote_id
A colour id from GET /v1/print-options whose materials include the quote's material. Without it, any colour of the material.
lines[].test_printboolean
with quote_id
One unit at the test print discount (cart setting). Only with quantity 1, once per part per account; the same file counts as the same part (ADR-0016).
lines[].skustring
required
with sku
lines[].materialenum
required
with sku
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
lines[].colourstring
required
with sku
Colour id from the catalog.
lines[].quantityinteger
required
with sku
1 to 100.
lines[].namestring
with sku
For products with personalisation required.
discount_codestringA discount code as the customer typed it. A code that does not apply answers 422 discount_code_rejected with the reason; check it first with POST /v1/orders/preview. 1 to 64 characters.
localeenumThe language the order is placed in (contract 0.28.0, ADR-0025): the website's, sv on the Swedish site and en under /en. Kustom's checkout opens in it (sv-SE or en-SE), our shipping option texts and Fraktjakt's service names and delivery times are in it, and later emails can use it. Carrier and pickup point names stay as they are. Default sv. One of sv, en.
Response fields (9)
Response fields
FIELDTYPEDESCRIPTION
linesarray of objects
required
Lines as on an order. Each also has its own amounts before and after its discounts in both VATs (contract 0.34.0), so a cart shows them without working out VAT itself: gross_inc_vat_ore (before discounts, incl VAT; total_ex_vat_ore is the same ex VAT), net_ex_vat_ore and net_inc_vat_ore (after every discount, including the line's share of a code).
offersarray of objects
required
Upsell offers, one per line that has a next volume tier.
discount_codeobject or null
required
totalsobject
required
free_shipping_from_inc_vat_oreinteger or null
required
Parts total incl VAT, after discounts, from which shipping is free. The threshold is compared incl VAT.
free_shipping_from_oreinteger or null
required
The same threshold ex VAT, for showing it ex VAT. Shipping is free by free_shipping_from_inc_vat_ore.
free_shipping_remaining_inc_vat_oreintegerWhat the parts still need for free shipping, incl VAT; 0 once the order has it (contract 0.34.0).
free_shipping_remaining_ex_vat_oreintegerThe same ex VAT, for showing it ex VAT.
settings_versionstring
required
The cart settings version used.
Errors (4)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
422A line cannot be ordered, with the same codes as POST /v1/orders.
REQUEST
curl https://api.fabrikenlabs.se/v1/orders/preview \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "quote_id": "quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80"
      }
    ]
  }'
RESPONSE200 OK
{
  "lines": [
    {
      "id": <string>,
      "kind": <string>,
      "material": <string>,
      "quantity": <integer>,
      "unit_price_ex_vat_ore": <integer>,
      "discount_ex_vat_ore": <integer>,
      "total_ex_vat_ore": <integer>
    }
  ],
  "offers": [
    {
      "line_index": <integer>,
      "to_quantity": <integer>,
      "discount": <number>,
      "new_total_ex_vat_ore": <integer>,
      "new_total_inc_vat_ore": <integer>,
      "saving_ex_vat_ore": <integer>,
      "saving_inc_vat_ore": <integer>,
      "message": <string>
    }
  ],
  "discount_code": <object or null>,
  "totals": {
    "items_ex_vat_ore": <integer>,
    "discount_ex_vat_ore": <integer>,
    "total_ex_vat_ore": <integer>,
    "vat_ore": <integer>,
    "total_inc_vat_ore": <integer>
  },
  "free_shipping_from_inc_vat_ore": <integer or null>,
  "free_shipping_from_ore": <integer or null>,
  "settings_version": <string>
}

Price list

The same data as our pricing page. Public, no key needed.

GET/v1/price-listLIVENO KEY

Current price list

Every price Fabriken charges, from the single source of truth (ADR-0011).

Response fields (9)
Response fields
FIELDTYPEDESCRIPTION
versionstring
required
effective_fromdate
required
currencystring
required
Always SEK.
vat_ratenumber
required
print_on_demandobject
required
materialsarray of objects
required
productionobject
required
rulesarray of string
required
manual_repair_feeobjectWhat a manual repair by Fabriken costs (contract 0.31.0), from the cart setting in force: charged as its own line on the order.
REQUEST
curl https://api.fabrikenlabs.se/v1/price-list
RESPONSE200 OK
{
  "version": <string>,
  "effective_from": <date>,
  "currency": "SEK",
  "vat_rate": <number>,
  "print_on_demand": {
    "start_fee_ore": <integer>,
    "per_machine_hour_ore": <integer>,
    "per_gram_ore": {
      "standard": <integer>,
      "engineering": <integer>
    }
  },
  "materials": [
    {
      "material": <string>,
      "name": <string>,
      "class": <string>,
      "per_gram_ore": <integer or null>,
      "pricing": <string>,
      "note": <string or null>
    }
  ],
  "production": {
    "setup_fee_ore": <integer>,
    "pick_and_pack_ore": {
      "min": <integer>,
      "max": <integer>
    }
  },
  "rules": [
    <string>
  ]
}

The API did not answer when this page was built, so amounts show their type.

POST/v1/pricesLIVENO KEY

Price preview from grams and print time

Applies the published price formula to known grams and minutes. This is not a quote: a quote is created from an uploaded file, is sliced on the server (ADR-0006) and expires after 24 hours.

Request body
FIELDTYPEDESCRIPTION
materialenum
required
Every material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
grams_per_unitnumber
required
Above 0.
minutes_per_unitnumber
required
Above 0.
quantityinteger
required
1 to 10000.
Response fields (9)
Response fields
FIELDTYPEDESCRIPTION
currencystring
required
Always SEK.
price_list_versionstring
required
start_fee_oreinteger
required
material_oreinteger
required
machine_time_oreinteger
required
rounding_oreinteger
required
total_ex_vat_oreinteger
required
vat_oreinteger
required
total_inc_vat_oreinteger
required
Errors (1)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, or code material_not_instant for a material without an instant price in the price list in force (with next_step: request_price).
REQUEST
curl https://api.fabrikenlabs.se/v1/prices \
  -H "Content-Type: application/json" \
  -d '{
    "material": "PETG",
    "grams_per_unit": 22.4,
    "minutes_per_unit": 96,
    "quantity": 10
  }'
RESPONSE200 OK
{
  "currency": "SEK",
  "price_list_version": <string>,
  "start_fee_ore": <integer>,
  "material_ore": <integer>,
  "machine_time_ore": <integer>,
  "rounding_ore": <integer>,
  "total_ex_vat_ore": <integer>,
  "vat_ore": <integer>,
  "total_inc_vat_ore": <integer>
}

The API did not answer when this page was built, so amounts show their type.

Shop

Our own designs at fixed retail prices, as sold in the shop. Public, no key needed.

GET/v1/catalogLIVENO KEY

Shop catalog

Our own designs with fixed retail prices (ADR-0017). Only materials whose sliced file is uploaded are listed: a product is sold only once its file exists. availability per colour is in_stock (ships directly) or printable (printed first).

Response fields (5)
Response fields
FIELDTYPEDESCRIPTION
versionstring
required
currencystring
required
Always SEK.
max_name_lengthinteger
required
name_fee_inc_vat_oreinteger or nullWhat an optional name costs per unit, incl VAT. A catalog setting; null until Pierre sets it.
productsarray of CatalogProduct
required
REQUEST
curl https://api.fabrikenlabs.se/v1/catalog
RESPONSE200 OK
{
  "version": <string>,
  "currency": "SEK",
  "max_name_length": <integer>,
  "products": [
    <CatalogProduct>
  ]
}
GET/v1/catalog/{sku}LIVENO KEY

A product's page in the shop

One product as the shop shows it: price, personalisation and its variants (material and colour) with availability. Only sellable products: one without an uploaded sliced file answers 404, as an unknown one does.

Parameters
FIELDTYPEDESCRIPTION
skustring · path
required
Response fields (17)
Response fields
FIELDTYPEDESCRIPTION
skustring
required
namestring
required
price_inc_vat_oreinteger
required
Retail price per unit incl VAT.
price_ex_vat_oreinteger
required
personalisationenum
required
required: the order line needs a name, and the item is always printed for the order. optional: a name may be given, for the catalog's name_fee_inc_vat_ore more per unit; with a name it is printed for the order. none: no name. One of none, optional, required.
categorystring or null
required
The shop's grouping, for example Desk, Home, Gifts or Christmas.
summarystring or null
required
One line for the product card.
descriptionstring or null
required
The product page's text.
category_svstring or nullThe category in Swedish, for the Swedish shop; null shows category (contract 0.21.0).
summary_svstring or nullThe card line in Swedish; null shows summary.
description_svstring or nullThe product page's text in Swedish; null shows description.
added_atdate-time or null
required
When the product was added, for "New first". Null for products from the seed.
has_modelboolean
required
GET /v1/catalog/{sku}/model has the model for the 3D view.
variantsarray of objects
required
One per material with an uploaded file, and colour.
max_name_lengthinteger
required
Longest name a customer can add, for personalised products.
name_fee_inc_vat_oreinteger or null
required
catalog_versionstring
required
Errors (1)
Errors
STATUSCODEWHEN
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/catalog/{sku}
RESPONSE200 OK
{
  "sku": <string>,
  "name": <string>,
  "price_inc_vat_ore": <integer>,
  "price_ex_vat_ore": <integer>,
  "personalisation": <string>,
  "category": <string or null>,
  "summary": <string or null>,
  "description": <string or null>,
  "added_at": <date-time or null>,
  "has_model": <boolean>,
  "variants": [
    {
      "material": <string>,
      "colour": <CatalogColour>,
      "availability": <string>,
      "in_stock": <integer>,
      "minutes_per_unit": <number>,
      "grams_per_unit": <number or null>,
      "size_mm": [
        <number>
      ]
    }
  ],
  "max_name_length": <integer>,
  "name_fee_inc_vat_ore": <integer or null>,
  "catalog_version": <string>
}
GET/v1/catalog/{sku}/modelLIVENO KEY

A product's model, for the 3D view

The model as it was uploaded (STL), from the part whose quote made the product sellable. For the product page's 3D view; there are no product photos. Of the first material with a file, or of material. Cached for an hour; the ETag is the file's SHA-256.

Parameters
FIELDTYPEDESCRIPTION
skustring · path
required
materialenum · queryEvery material the price list names. Only those with an instant price in the price list in force get a price; the others answer 400 with code material_not_instant and show "Request price". TPU is Request price from price list 2026-11-07. One of PLA, PETG, TPU, PLA-CF, PETG-CF.
Errors (1)
Errors
STATUSCODEWHEN
404product_not_foundmodel_not_foundNo such product (product_not_found), or no model yet (model_not_found).
REQUEST
curl https://api.fabrikenlabs.se/v1/catalog/{sku}/model
RESPONSE200 OK
<binary>
GET/v1/capacityLIVENO KEY

The live print time (public)

Printers online and when a job paid now would start, for the website and the app ("Prints now", "Prints tonight", "Order now, prints within 10 h"). Printing runs around the clock: the time comes from the print room's queue, the printers' states and the plate clear time, and is worded by the print_room setting (prints_now_within_minutes, prints_tonight_from_hour, time_zone). Real scarcity (fabriken-hq decisions): only from a live print room that has reported within offline_alert_minutes; from a dry run, a stale snapshot, or none, the answer is 503 capacity_unavailable and the website shows no numbers and no time at all. next_print is null when no printer can take a job. Never cached.

Response fields (4)
Response fields
FIELDTYPEDESCRIPTION
printers_onlineinteger
required
fleet_sizeinteger
required
next_printobject or null
required
When a job paid now would start, or null when no printer can take one.
as_ofdate-time
required
When the orchestrator made the snapshot.
Errors (1)
Errors
STATUSCODEWHEN
503capacity_unavailableNo live, fresh capacity. Code capacity_unavailable.
REQUEST
curl https://api.fabrikenlabs.se/v1/capacity
RESPONSE200 OK
{
  "printers_online": <integer>,
  "fleet_size": <integer>,
  "next_print": {
    "starts_at": <date-time>,
    "status": <string>,
    "hours": <integer>
  },
  "as_of": <date-time>
}

MCP serverCOMING

Connect Claude or any MCP client. The agent signs in with your account and uses one of your keys, so its limits and approvals apply.

  • Agents never see card details.
  • Orders above the approval threshold always wait for you.
  • Every agent order shows which key placed it.
MCPCOMING
// MCP client config
"fabriken": { "url": "https://api.fabrikenlabs.se/mcp" }

// Tools
get_price_list     read the price list
quote_part         file + settings to a 24 h quote
get_capacity       printers online and the live print time
place_order        within the key's limits
order_status       status, printer, camera link

WebhooksLIVE

We POST events to your URL, signed with your endpoint's secret in the Fabriken-Signature header. Verify it before trusting the body. The account's owners and admins create endpoints, signed in to the app; API keys cannot.

Webhook events
EVENTDATAWHEN
quote.readyquoteA quote is ready
order.paidorderAn order is paid
order.printingorderAn order's first part is on a printer
order.shippedorder, trackingAn order has shipped, with tracking
order.cancelledorderAn order is cancelled
price_request.answeredprice_request, quoteFabriken answered a price request with a quote
price_request.declinedprice_requestFabriken declined a price request
EVENT
POST https://your-app.example/fabriken
Content-Type: application/json
Fabriken-Signature: t=1793990000,v1=5c2e…
Fabriken-Event-Id: evt_3f0c9a1e5b7d4c2a8e6f1b0d9c7a5e3f
Fabriken-Event-Type: order.shipped
Fabriken-Delivery-Id: whdl_7a1c3e5b9d2f4a6c8e0b1d3f5a7c9e2b
Fabriken-Delivery-Attempt: 1

{
  "id": "evt_3f0c9a1e5b7d4c2a8e6f1b0d9c7a5e3f",
  "type": "order.shipped",
  "created_at": "2026-11-06T18:33:20.000Z",
  "account_id": <string>,
  "data": {
    "order": <Order>,
    "tracking": {
      "url": <string or null>,
      "carrier": <string or null>,
      "service": <string or null>
    }
  }
}

Verify the signature

t=<unix seconds>,v1=<hex>: HMAC-SHA256 with the endpoint's signing secret (whsec_..., the whole string as the key) over <t>.<raw body>. Recompute it over the exact bytes received, compare in constant time, and refuse a t more than 5 minutes from your clock. Several v1= values may appear; one match is enough.

NODE
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the exact bytes received, as a string, not a re-serialised object.
export function verify(secret, rawBody, header, now = Date.now()) {
  const parts = Object.groupBy(header.split(","), (p) => p.split("=")[0]);
  const t = Number(parts.t?.[0]?.slice(2));
  if (!t || Math.abs(now / 1000 - t) > 300) return false; // a replay
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return (parts.v1 ?? []).some((v) => {
    const given = Buffer.from(v.slice(3), "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Retries, duplicates and the delivery log

One event to one endpoint. pending until a 2xx answer (succeeded) or the last of max_attempts attempts (failed). Retries wait 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h and 8 h (each spread by up to 10%): about 24 hours in all. That is the default; Fabriken sets the schedule and the 10 s timeout in its connections setting.

The event's id (evt_...), the same as the body's id, in every attempt and redelivery. Process each id once. Every delivery also sends Fabriken-Event-Type, Fabriken-Delivery-Id and Fabriken-Delivery-Attempt.

POST/v1/webhooksLIVEAPI KEY

Create a webhook endpoint

Fabriken POSTs the chosen events to url (ADR-0024), signed with HMAC-SHA256 in the Fabriken-Signature header (see the webhooks section of this contract). The answer holds the signing secret (whsec_...): it is shown only here. Keep it; Fabriken cannot show it again (the same create sent again with the same Idempotency-Key returns it, for a lost answer). To change it, delete the endpoint and create a new one.

Owners and admins of the account, signed in (account:manage, ADR-0021); API keys cannot manage webhooks.

On servers the URL must be https and reach a public address; local and private addresses are refused (422 invalid_webhook_url). At most 10 endpoints per account by default (Fabriken's connections setting; 409 too_many_webhooks).

Parameters
FIELDTYPEDESCRIPTION
Idempotency-Keystring · header
required
A unique value per create request, for example a UUID. See Idempotency.
Request body
FIELDTYPEDESCRIPTION
urlstring
required
https on servers, for example https://example.com/fabriken/webhooks. At most 2048 characters.
eventsarray of enum
required
descriptionstringAt most 200 characters.
Response fields (7)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
urlstring
required
descriptionstring or null
required
eventsarray of enum
required
secret_last4string
required
The signing secret's last four characters, to tell secrets apart.
created_atdate-time
required
secretstring
required
The signing secret. Shown only when the endpoint is created.
Errors (6)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
409too_many_webhooksidempotency_key_in_progressCode too_many_webhooks: the account has its most endpoints (10 by default). Or idempotency_key_in_progress.
422invalid_webhook_urlidempotency_key_reusedCode invalid_webhook_url (with field: url): the message says why. Or idempotency_key_reused.
503webhooks_unavailableThe server cannot protect signing secrets right now. Code webhooks_unavailable.
REQUEST
curl https://api.fabrikenlabs.se/v1/webhooks \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Idempotency-Key: 9b1d4e2a-6c3f-4a8e-b5d7-2f0c1e9a8b64" \
  -H "Content-Type: application/json" \
  -d '{
    "url": <string>,
    "events": [
      <string>
    ]
  }'
RESPONSE201 CREATED
{
  "id": <string>,
  "url": <string>,
  "description": <string or null>,
  "events": [
    <string>
  ],
  "secret_last4": <string>,
  "created_at": <date-time>,
  "secret": <string>
}
GET/v1/webhooksLIVEAPI KEY

List the account's webhook endpoints

Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
webhooksarray of WebhookEndpoint
required
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
REQUEST
curl https://api.fabrikenlabs.se/v1/webhooks \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "webhooks": [
    <WebhookEndpoint>
  ]
}
GET/v1/webhooks/{webhook_id}LIVEAPI KEY

Get a webhook endpoint

Parameters
FIELDTYPEDESCRIPTION
webhook_idstring · path
required
Response fields (6)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
urlstring
required
descriptionstring or null
required
eventsarray of enum
required
secret_last4string
required
The signing secret's last four characters, to tell secrets apart.
created_atdate-time
required
Errors (3)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/webhooks/{webhook_id} \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": <string>,
  "url": <string>,
  "description": <string or null>,
  "events": [
    <string>
  ],
  "secret_last4": <string>,
  "created_at": <date-time>
}
DELETE/v1/webhooks/{webhook_id}LIVEAPI KEY

Delete a webhook endpoint

Stops every delivery to it, pending ones included, and deletes its delivery log.

Parameters
FIELDTYPEDESCRIPTION
webhook_idstring · path
required
Errors (3)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl -X DELETE https://api.fabrikenlabs.se/v1/webhooks/{webhook_id} \
  -H "Authorization: Bearer fab_live_••••8f2c"
GET/v1/webhooks/{webhook_id}/deliveriesLIVEAPI KEY

The endpoint's delivery log

Deliveries newest first, with the last attempt of each.

Parameters
FIELDTYPEDESCRIPTION
webhook_idstring · path
required
statusenum · queryOne of pending, succeeded, failed.
limitinteger · query1 to 200. Default 50.
Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
deliveriesarray of WebhookDelivery
required
Errors (4)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/webhooks/{webhook_id}/deliveries \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "deliveries": [
    <WebhookDelivery>
  ]
}
GET/v1/webhooks/{webhook_id}/deliveries/{delivery_id}LIVEAPI KEY

One delivery with every attempt and the event sent

Parameters
FIELDTYPEDESCRIPTION
webhook_idstring · path
required
delivery_idstring · path
required
Response fields (14)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
event_idstring
required
event_typeenum
required
One of quote.ready, order.paid, order.printing, order.shipped, order.cancelled, price_request.answered, price_request.declined.
endpoint_idstring
required
statusenum
required
One of pending, succeeded, failed.
attempt_countinteger
required
max_attemptsinteger
required
next_attempt_atdate-time or null
required
last_attemptWebhookAttempt or null
required
redelivery_ofstring or null
required
The delivery this one sends again.
created_atdate-time
required
finished_atdate-time or null
required
attemptsarray of WebhookAttempt
required
Oldest first, at most the newest 20.
eventWebhookEvent
required
The body of every delivery. id is the same in every attempt and redelivery of the event: process each id once.
Errors (3)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/webhooks/{webhook_id}/deliveries/{delivery_id} \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "id": <string>,
  "event_id": <string>,
  "event_type": <string>,
  "endpoint_id": <string>,
  "status": <string>,
  "attempt_count": <integer>,
  "max_attempts": <integer>,
  "next_attempt_at": <date-time or null>,
  "last_attempt": <WebhookAttempt>,
  "redelivery_of": <string or null>,
  "created_at": <date-time>,
  "finished_at": <date-time or null>,
  "attempts": [
    <WebhookAttempt>
  ],
  "event": <WebhookEvent>
}
POST/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliverLIVEAPI KEY

Send a delivery again

A new delivery of the same event to the same endpoint, due now, with a fresh set of attempts. The event id stays the same, so a receiver that already processed it can skip it.

Parameters
FIELDTYPEDESCRIPTION
webhook_idstring · path
required
delivery_idstring · path
required
Response fields (12)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
event_idstring
required
event_typeenum
required
One of quote.ready, order.paid, order.printing, order.shipped, order.cancelled, price_request.answered, price_request.declined.
endpoint_idstring
required
statusenum
required
One of pending, succeeded, failed.
attempt_countinteger
required
max_attemptsinteger
required
next_attempt_atdate-time or null
required
last_attemptWebhookAttempt or null
required
redelivery_ofstring or null
required
The delivery this one sends again.
created_atdate-time
required
finished_atdate-time or null
required
Errors (3)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
404part_not_foundquote_not_foundorder_not_foundjob_not_foundproduct_not_foundprice_request_not_foundwebhook_not_founddelivery_not_foundNo such resource, or it belongs to another account. Code part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, price_request_not_found, webhook_not_found or delivery_not_found.
REQUEST
curl -X POST https://api.fabrikenlabs.se/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE202 ACCEPTED
{
  "id": <string>,
  "event_id": <string>,
  "event_type": <string>,
  "endpoint_id": <string>,
  "status": <string>,
  "attempt_count": <integer>,
  "max_attempts": <integer>,
  "next_attempt_at": <date-time or null>,
  "last_attempt": <WebhookAttempt>,
  "redelivery_of": <string or null>,
  "created_at": <date-time>,
  "finished_at": <date-time or null>
}

More endpoints

Newer calls in the contract.

GET/v1/print-optionsLIVENO KEY

What a quote can choose

The layer heights and the infill range a quote may ask for, with the defaults a quote gets when it asks for none, and the colours we have spools of per material. All are business settings (ADR-0018): the print_options setting and the catalog setting's colours. Send a colour's id with an order's quote line; only a colour whose materials include the quote's material is accepted.

Response fields (6)
Response fields
FIELDTYPEDESCRIPTION
versionstring
required
The print_options setting version.
catalog_versionstring
required
The catalog setting version the colours come from.
layer_heightsarray of objects
required
In the order a screen shows them.
default_layer_height_mmnumber
required
infillobject
required
coloursarray of PrintColour
required
REQUEST
curl https://api.fabrikenlabs.se/v1/print-options
RESPONSE200 OK
{
  "version": "seed-2026-11-07",
  "catalog_version": "seed-2026-11-06",
  "layer_heights": [
    {
      "label": "Draft",
      "layer_height_mm": 0.24,
      "process_preset": "0.24mm Standard @BBL X2D"
    },
    {
      "label": "Standard",
      "layer_height_mm": 0.2,
      "process_preset": "0.20mm Standard @BBL X2D"
    },
    {
      "label": "Fine",
      "layer_height_mm": 0.12,
      "process_preset": "0.12mm High Quality @BBL X2D"
    }
  ],
  "default_layer_height_mm": 0.2,
  "infill": {
    "min_percent": 10,
    "max_percent": 100,
    "step_percent": 5,
    "default_percent": 25
  },
  "colours": [
    {
      "id": "black",
      "name": "Black",
      "hex": "#000000",
      "materials": [
        "PLA",
        "PETG"
      ]
    },
    {
      "id": "lime",
      "name": "Lime",
      "hex": "#C0DF16",
      "materials": [
        "PETG"
      ]
    }
  ]
}

The API did not answer when this page was built, so amounts show their type.

GET/v1/upload-limitsLIVEAPI KEY

What an upload may be

The largest file POST /v1/parts takes, the most parts one 3MF file may make, and the formats. From the uploads setting (ADR-0018), so a client can check a file before sending it.

Response fields (4)
Response fields
FIELDTYPEDESCRIPTION
versionstring
required
The uploads setting version.
max_upload_bytesinteger
required
max_objectsinteger
required
Most parts one 3MF file may make.
formatsarray of enum
required
Errors (1)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
REQUEST
curl https://api.fabrikenlabs.se/v1/upload-limits \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "version": <string>,
  "max_upload_bytes": <integer>,
  "max_objects": <integer>,
  "formats": [
    <string>
  ]
}
GET/v1/appearanceLIVENO KEY

The website's theme settings (public)

The appearance setting in force (ADR-0018): the default theme mode and the place whose sunrise and sunset Auto follows. The website reads it before it draws a page; without an answer it uses the seed (Auto, Helsingborg, 10 minutes each way). Cached for 60 seconds.

Response fields (5)
Response fields
FIELDTYPEDESCRIPTION
versionstring
default_modeenum
required
What a new visitor gets. system follows the device's light or dark setting. One of auto, light, dark, system.
locationobject
required
dark_after_sunset_minutesinteger
required
0 to 180.
light_before_sunrise_minutesinteger
required
0 to 180.
REQUEST
curl https://api.fabrikenlabs.se/v1/appearance
RESPONSE200 OK
{
  "default_mode": <string>,
  "location": {
    "name": <string>,
    "latitude": <number>,
    "longitude": <number>
  },
  "dark_after_sunset_minutes": <integer>,
  "light_before_sunrise_minutes": <integer>
}
GET/v1/analyticsLIVENO KEY

The website's analytics and site verification settings (public)

The analytics setting in force (ADR-0018): the PostHog project key and host the website uses after a visitor accepts cookies, whether the public site and the shop are recorded, and the values of the Google and Bing site verification meta tags. Nothing in it is a secret: a PostHog project key is sent to every browser by design. An empty key means no analytics at all (no script, no cookie, no banner); an empty verification value means no tag. The website reads it before it draws a page and has no analytics when there is no answer. Cached for 60 seconds.

Response fields (3)
Response fields
FIELDTYPEDESCRIPTION
versionstring
posthogobject
required
verificationobject
required
REQUEST
curl https://api.fabrikenlabs.se/v1/analytics
RESPONSE200 OK
{
  "posthog": {
    "project_key": <string>,
    "host": <string>,
    "session_replay": <boolean>
  },
  "verification": {
    "google": <string>,
    "bing": <string>
  }
}
GET/v1/email-previewsLIVEAPI KEY

Every email, with sample data

Every email Fabriken sends, rendered with made-up sample data, for the ops page Emails (contract 0.29.0): customer emails in Swedish and English, staff emails in English, each as its subject, HTML and plain text, with the sender in force. Staff only.

Response fields (2)
Response fields
FIELDTYPEDESCRIPTION
senderobject
required
previewsarray of objects
required
Errors (2)
Errors
STATUSCODEWHEN
401unauthorizedNo API key, or the key is unknown or revoked. Code unauthorized. Send Authorization: Bearer fab_live_... with a valid key.
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
REQUEST
curl https://api.fabrikenlabs.se/v1/email-previews \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "sender": {
    "name": <string>,
    "address": <string>,
    "reply_to": <string or null>
  },
  "previews": [
    {
      "id": <string>,
      "template": <string>,
      "audience": <string>,
      "title": <string>,
      "when": <string>,
      "locale": <string>,
      "subject": <string>,
      "html": <string>,
      "text": <string>
    }
  ]
}
POST/v1/contactLIVENO KEY

Send a message from the website's contact form

Emails the message to Fabriken: the addresses under "Request price and contact form" on the Settings page (the notifications setting, price_request_emails). Nothing is stored. To get a part priced, upload it and use POST /v1/price-requests instead.

Public, so it is guarded (contract 0.27.0). website is a field a form hides from people: a message with anything in it is dropped. So is one sent less than 3 seconds after the form opened (form_seconds, sent by the website). A dropped message gets the same answer as a sent one. At most 5 messages an hour per visitor address and per sender address, and 100 an hour in all.

Request body
FIELDTYPEDESCRIPTION
namestring
required
1 to 200 characters.
emailstring
required
Where the answer goes. At most 254 characters.
companystringAt most 200 characters.
topicenum
required
One of pilot, request_price, api_access, other.
messagestring
required
1 to 5000 characters.
part_idstringA part the sender uploaded, named in the email.
localeenumThe language the sender wrote from, so the reply can match. One of sv, en.
websitestringHoneypot. Leave it out, or empty. At most 500 characters.
form_secondsintegerWhole seconds the form was open before sending. Under 3 drops the message. At least 0.
Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
idstring
required
Errors (3)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
429too_many_attemptsToo many attempts in a short time. Code too_many_attempts.
503contact_unavailableCode contact_unavailable: nobody is set to receive messages, or the email could not be sent. Nothing was kept; try again or email.
REQUEST
curl https://api.fabrikenlabs.se/v1/contact \
  -H "Content-Type: application/json" \
  -d '{
    "name": <string>,
    "email": <string>,
    "topic": <string>,
    "message": <string>
  }'
RESPONSE202 ACCEPTED
{
  "id": <string>
}
GET/v1/support/chatLIVEKEY OPTIONAL

The caller's support chat (customers)

The support chat of the signed-in person (their session), or of a guest (the Fabriken-Chat-Token header, which the website keeps in an httpOnly cookie). chat is null until they write. Nobody reads another person's chat. API keys and machine tokens are refused with chat_for_people: the chat is for people on the website (ADR-0026). availability says whether staff are in by the opening hours of the support setting, with its offline text when they are not.

Parameters
FIELDTYPEDESCRIPTION
Fabriken-Chat-Tokenstring · headerA guest's support chat token, from the answer that started their chat. The website keeps it in an httpOnly cookie. Ignored for a signed-in person, whose chat is found by their session.
localeenum · queryThe language of availability.notice. Default the chat's, else Swedish. One of sv, en.
Response fields (4)
Response fields
FIELDTYPEDESCRIPTION
signed_inboolean
required
A signed-in person, known by their session. A guest gives an email first.
chatSupportChat or null
required
availabilitySupportAvailability
required
max_message_charsinteger
required
The longest message, from the support setting.
Errors (1)
Errors
STATUSCODEWHEN
403forbiddenaccount_requiredsecond_factor_requiredkey_role_retiredThe caller may not call this. Code forbidden; account_required for a person without an account yet; second_factor_required for staff who must confirm their authenticator first; key_role_retired for an old admin or printer room key.
REQUEST
curl https://api.fabrikenlabs.se/v1/support/chat \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "signed_in": <boolean>,
  "chat": <SupportChat>,
  "availability": <SupportAvailability>,
  "max_message_chars": <integer>
}
POST/v1/support/chat/messagesLIVEKEY OPTIONAL

Write to Fabriken in the support chat (customers)

Adds a message to the caller's chat, or starts one. A guest's first message needs email, so a late answer can reach them; the answer then carries chat_token once, which the website keeps in an httpOnly cookie and sends back in Fabriken-Chat-Token. A new or reopened chat gets the auto-reply (within opening hours) or the offline text (outside them) from the support setting, as a system message. Writing to a closed chat reopens it, unless staff blocked it (chat_blocked).

context says where the customer is. The API keeps only what it finds in the caller's own account (a part, a quote, an order) or a guest order whose follow token is sent as order_token; the path is kept without its query. The cart is product lines only; staff see it priced by the API. Nothing the browser says about prices is ever used.

Guarded like the contact form: website is a honeypot, and a guest's new chat sent under 3 seconds after the page opened (form_seconds) is dropped; both get an answer that looks the same and keep nothing. Messages per chat, new chats per visitor address and new chats in all per hour, and the longest message, are in the support setting.

Parameters
FIELDTYPEDESCRIPTION
Fabriken-Chat-Tokenstring · headerA guest's support chat token, from the answer that started their chat. The website keeps it in an httpOnly cookie. Ignored for a signed-in person, whose chat is found by their session.
Request body
FIELDTYPEDESCRIPTION
bodystring
required
At most the support setting's max_message_chars. 1 to 10000 characters.
localeenumThe page's language. The auto-reply and a late email follow it. One of sv, en.
contextSupportContextInputWhere the customer is, as the website sees it. Only ids the API finds in the caller's account, or a guest order with its token, are kept.
emailstringA guest's email. Needed for a guest's first message. At most 254 characters.
namestringAt most 200 characters.
websitestringHoneypot. Leave it out, or empty. At most 500 characters.
form_secondsintegerWhole seconds the page was open before a guest's first message. At least 0.
Response fields (3)
Response fields
FIELDTYPEDESCRIPTION
chatSupportChat
required
A support chat as the customer sees it.
messageSupportMessage
required
A message as the customer sees it.
chat_tokenstringA new guest chat's token, shown once. Keep it in an httpOnly cookie.
Errors (4)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
403chat_blockedchat_for_peoplechat_blocked: staff closed this chat for new messages. chat_for_people: an API key or machine token.
422email_requiredmessage_too_longemail_required for a guest's first message, message_too_long.
429too_many_messagessupporttoo_many_messages: a limit of the support setting.
REQUEST
curl https://api.fabrikenlabs.se/v1/support/chat/messages \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Content-Type: application/json" \
  -d '{
    "body": <string>
  }'
RESPONSE201 CREATED
{
  "chat": <SupportChat>,
  "message": <SupportMessage>
}
PUT/v1/support/chat/contextLIVEKEY OPTIONAL

Tell staff where the customer is now (customers)

The website sends this when a customer with a chat moves to another page, so the staff's context panel follows them. Checked like a message's context. Page changes per chat per hour are limited by the support setting.

Parameters
FIELDTYPEDESCRIPTION
Fabriken-Chat-Tokenstring · headerA guest's support chat token, from the answer that started their chat. The website keeps it in an httpOnly cookie. Ignored for a signed-in person, whose chat is found by their session.
Request body
FIELDTYPEDESCRIPTION
pathstringThe page's path. Its query and fragment are dropped. At most 500 characters.
part_idstring
quote_idstring
order_idstring
order_tokenstringA guest order's follow token, or the link token from an order email. Never stored.
skustringA shop product page.
cartobjectThe shop cart, product lines only. Priced by the API for staff.
cart.linesarray of objects
required
cart.lines[].skustring
required
cart.lines[].materialstring
required
cart.lines[].colourstring
required
cart.lines[].quantityinteger
required
1 to 100.
cart.lines[].namestring
cart.discount_codestringAt most 64 characters.
Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
atdate-time
required
Errors (3)
Errors
STATUSCODEWHEN
400material_not_instantInvalid input, for example a missing Idempotency-Key, an unknown field, an empty upload, or a material without an instant price (material_not_instant).
404chat_not_foundchat_not_found: the caller has no chat yet.
429too_many_messagestoo_many_messages.
REQUEST
curl -X PUT https://api.fabrikenlabs.se/v1/support/chat/context \
  -H "Authorization: Bearer fab_live_••••8f2c" \
  -H "Content-Type: application/json" \
  -d '{}'
RESPONSE200 OK
{
  "at": <date-time>
}
POST/v1/support/chat/readLIVEKEY OPTIONAL

The customer has read the chat (customers)

Marks every message as read by the customer. A staff reply the customer has read is never emailed.

Parameters
FIELDTYPEDESCRIPTION
Fabriken-Chat-Tokenstring · headerA guest's support chat token, from the answer that started their chat. The website keeps it in an httpOnly cookie. Ignored for a signed-in person, whose chat is found by their session.
Response fields (1)
Response fields
FIELDTYPEDESCRIPTION
read_atdate-time
required
Errors (1)
Errors
STATUSCODEWHEN
404chat_not_foundchat_not_found.
REQUEST
curl -X POST https://api.fabrikenlabs.se/v1/support/chat/read \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
{
  "read_at": <date-time>
}
GET/v1/support/chat/eventsLIVEKEY OPTIONAL

Live events of the customer's chat (customers, Server-Sent Events)

A text/event-stream that stays open. Events: ready once ({ chat_id }), message with a SupportMessage (staff replies, system texts and the customer's own messages from other windows), and chat with { status, blocked } when staff close or reopen it. A comment every 20 seconds keeps proxies from closing it, and retry: 2000 makes a browser reconnect after 2 seconds. While a stream is open the customer counts as present, so staff replies are not emailed. Clients poll GET /v1/support/chat when it drops.

Parameters
FIELDTYPEDESCRIPTION
Fabriken-Chat-Tokenstring · headerA guest's support chat token, from the answer that started their chat. The website keeps it in an httpOnly cookie. Ignored for a signed-in person, whose chat is found by their session.
Errors (1)
Errors
STATUSCODEWHEN
404chat_not_foundchat_not_found.
REQUEST
curl https://api.fabrikenlabs.se/v1/support/chat/events \
  -H "Authorization: Bearer fab_live_••••8f2c"
RESPONSE200 OK
<string>

Errors

Errors return the HTTP status, a stable code, a message a person can read and, where it helps, the next step. Unknown fields and query parameters are rejected, not ignored, so typos fail loudly.

Error fields
FIELDTYPEDESCRIPTION
statusCodeinteger
required
codestringMachine-readable reason: unauthorized, forbidden, part_not_found, quote_not_found, order_not_found, job_not_found, product_not_found, the order, stock and job codes listed with each operation, part_not_quotable, material_not_instant, empty_upload, file_too_large, unsupported_media_type, idempotency_key_reused, idempotency_key_in_progress, slicer_unavailable, or FST_ERR_VALIDATION for a request that does not match this contract.
errorstring
required
The HTTP reason phrase.
messagestring
required
What went wrong, in words a person can read.
next_stepenumOnly on part_not_quotable and material_not_instant. One of quote, repair, fix_and_upload, request_price.
retry_after_secondsintegerOnly on shipping_unavailable at checkout. When to try again.

Stable codes

Error codes
STATUSCODEWHERE
400invalid_cursorGET /v1/parts
400material_not_instantPOST /v1/prices, POST /v1/parts, POST /v1/quotes and 9 more
401unauthorizedGET /v1/parts, POST /v1/parts, GET /v1/upload-limits and 21 more
403account_requiredGET /v1/orders, POST /v1/orders, GET /v1/email-previews and 8 more
403approval_requiredPOST /v1/orders
403chat_blockedPOST /v1/support/chat/messages
403chat_for_peoplePOST /v1/support/chat/messages
403forbiddenGET /v1/orders, POST /v1/orders, GET /v1/email-previews and 8 more
403key_role_retiredGET /v1/orders, GET /v1/email-previews, GET /v1/support/chat and 7 more
403second_factor_requiredGET /v1/orders, GET /v1/email-previews, GET /v1/support/chat and 7 more
403spend_limit_exceededPOST /v1/orders
404chat_not_foundPUT /v1/support/chat/context, POST /v1/support/chat/read, GET /v1/support/chat/events
404delivery_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
404job_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
404model_not_foundGET /v1/catalog/{sku}/model
404order_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
404part_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
404price_request_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
404product_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 15 more
404quote_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
404webhook_not_foundGET /v1/parts/{part_id}, GET /v1/parts/{part_id}/model, POST /v1/parts/{part_id}/repair and 14 more
409amount_mismatchPOST /v1/orders/{order_id}/confirm
409idempotency_key_in_progressPOST /v1/parts, POST /v1/quotes, POST /v1/orders and 2 more
409repair_not_neededPOST /v1/parts/{part_id}/repair
409too_many_webhooksPOST /v1/webhooks
413file_too_largePOST /v1/parts
413, 422uploadsPOST /v1/parts
415unsupported_media_typePOST /v1/parts
422colour_not_availablePOST /v1/orders
422email_requiredPOST /v1/support/chat/messages
422idempotency_key_reusedPOST /v1/parts, POST /v1/quotes, POST /v1/orders and 2 more
422instant_price_availablePOST /v1/price-requests
422invalid_webhook_urlPOST /v1/webhooks
422mesh_check_timed_outPOST /v1/parts
422message_too_longPOST /v1/support/chat/messages
422name_not_allowedPOST /v1/orders
422name_requiredPOST /v1/orders
422name_too_longPOST /v1/orders
422no_mesh_objectsPOST /v1/parts
422not_repairablePOST /v1/parts/{part_id}/repair
422part_not_quotablePOST /v1/quotes
422product_not_availablePOST /v1/orders
422quote_expiredPOST /v1/orders
422quote_needs_api_keyPOST /v1/orders
422quote_not_failedPOST /v1/price-requests
422quote_not_readyPOST /v1/orders
422too_many_objectsPOST /v1/parts
422unreadable_3mfPOST /v1/parts
422unsafe_archivePOST /v1/parts
429supportPOST /v1/support/chat/messages
429too_many_attemptsPOST /v1/contact
429too_many_messagesPOST /v1/support/chat/messages, PUT /v1/support/chat/context
502payment_failedPOST /v1/orders, POST /v1/orders/{order_id}/confirm
503capacity_unavailableGET /v1/capacity
503checkout_unavailablePOST /v1/orders
503contact_unavailablePOST /v1/contact
503payment_unavailablePOST /v1/orders, POST /v1/orders/{order_id}/confirm
503shipping_unavailablePOST /v1/orders
503slicer_unavailablePOST /v1/quotes
503webhooks_unavailablePOST /v1/webhooks
not statedempty_uploadNamed in the Error schema
not statedFST_ERR_VALIDATIONNamed in the Error schema
400 BAD REQUESTPOST /v1/prices
{
  "statusCode": 400,
  "code": "material_not_instant",
  "error": "Bad Request",
  "message": <string>,
  "next_step": "request_price"
}

The API did not answer when this page was built, so amounts show their type.

Example ids such as quote_8f2c41aa6e3b4d7c9a1f0e2d5b6c7a80 and the key fab_live_••••8f2c are made up. Amounts show their type: the API did not answer when this page was built.