API

Start here

This is a cut-list optimiser for sheet materials. You send finished part sizes in millimetres and a board size; it works out how many boards are needed and where every part sits on every board, then turns that plan into a drawing, a DXF or a machine program.

No key, no account, no registration. Post JSON, get JSON. Nothing you send is stored — no project row, no cut list on disk, no cookie — so there is nothing to delete afterwards and nothing to come back for later.

The smallest useful request — four shelves out of one 8x4 board:

curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/optimise' \
  -H 'Content-Type: application/json' \
  -d '{
  "materials": [
    {
      "ref": "mdf18",
      "panel": { "width": 1220, "length": 2440 },
      "parts": [{ "name": "shelf", "length": 600, "width": 400, "qty": 4 }]
    }
  ]
}'

The same thing from JavaScript

const res = await fetch('https://cut.uniboards.co.uk/v2/api/optimise', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "materials": [
      {
        "ref": "mdf18",
        "panel": { "width": 1220, "length": 2440 },
        "parts": [{ "name": "shelf", "length": 600, "width": 400, "qty": 4 }]
      }
    ]
  }),
});
const { ok, summary } = await res.json();
if (!ok) throw new Error('the service refused that cut list');

console.log(summary.boards_total);          // 1
console.log(summary.materials[0].sheets[0].parts[0]);
// { name: 'shelf (1)', part_id: '0_0', number: 1, x: 0, y: 0, width: 600, height: 400, rotated: true }

And the answer, field by field

{
  "ok": true,
  "engine": { "version": "V40", "algorithm": "guillotine", "ms": 0.4 },
  "summary": {
    "boards_total": 1,
    "parts_placed": 4,
    "parts_requested": 4,
    "unplaced": 0,
    "average_occupancy_pct": 32.249,
    "cuttable_on_saw": true,
    "offcuts": 1,
    "offcut_area_mm2": 1969578,
    "largest_offcut_mm2": 1969578,
    "scrap_area_mm2": 47320,
    "free_area_mm2": 2016898,
    "materials": [
      {
        "ref": "mdf18",
        "title": null,
        "can_rotate": true,
        "panel": { "width": 1220, "length": 2440 },
        "boards": 1,
        "occupancy_pct": 32.249,
        "sheets": [
          {
            "board": 1,
            "occupancy_pct": 32.249,
            "waste_pct": 67.8,
            "offcuts": 1,
            "offcut_area_mm2": 1969578,
            "largest_offcut_mm2": 1969578,
            "scrap_area_mm2": 47320,
            "free_area_mm2": 2016898,
            "parts": [
              {
                "name": "shelf (1)",
                "part_id": "0_0",
                "number": 1,
                "x": 0,
                "y": 0,
                "width": 600,
                "height": 400,
                "rotated": true
              },
              {
                "name": "shelf (2)",
                "part_id": "0_0",
                "number": 2,
                "x": 0,
                "y": 404.5,
                "width": 600,
                "height": 400,
                "rotated": true
              },
              {
                "name": "shelf (3)",
                "part_id": "0_0",
                "number": 3,
                "x": 0,
                "y": 809,
                "width": 600,
                "height": 400,
                "rotated": true
              },
              {
                "name": "shelf (4)",
                "part_id": "0_0",
                "number": 4,
                "x": 0,
                "y": 1213.5,
                "width": 600,
                "height": 400,
                "rotated": true
              }
            ]
          }
        ]
      }
    ]
  }
}
  • engine — which build of the nesting engine produced this and how long it took. Informational.
  • summary.boards_totalhow many boards to buy. Across every material in the request.
  • summary.parts_placed / parts_requested / unplaced — did everything fit. unplaced above zero means some parts are NOT on any board; the plan is still returned.
  • summary.average_occupancy_pct — how much of the bought board is covered by parts. 32.249 here means two thirds of that board is untouched, because four shelves do not fill an 8x4.
  • summary.cuttable_on_saw — whether a panel saw can reach every part, or whether this plan needs a CNC router. Worth showing to anyone with a saw.
  • summary.offcuts, offcut_area_mm2, largest_offcut_mm2, scrap_area_mm2what the waste actually is. An offcut counts as usable when both of its sides clear the threshold (300 mm by default), so it can be racked and cut again; everything else is scrap. Present per plan, per material and per sheet. Areas are in square millimetres — divide by 1 000 000 for m². Absent when the engine did not report them.
  • sheets[].waste_pct — the same number as occupancy, read the other way round. People fix waste, not occupancy: a sheet showing 88 % waste is the one to look at.
  • materials[].boards and materials[].sheets[]the boards themselves. One entry per physical board, numbered from 1.
  • sheets[].parts[]the placement. x and y are the bottom-left corner of the part on that board in millimetres, width and height are how it lies there, and rotated: true means it was turned 90° from the size you asked for. A part with can_rotate: false is never rotated.
  • Offcuts are not in this answer. They are whatever the parts do not cover: work them out from the rectangles above, or let /api/layout-pdf draw them — it marks the three largest usable pieces on every board with their sizes.
Every door this service has

This list is built from the routers themselves when the page is served, so it cannot describe a door that no longer exists or miss one that was just added. Every one of them carries a description. Every door anyone can call without an account is worked through below, with a request and the answer it really produced.

Everything is JSON in and JSON out unless the row says otherwise, everything is anonymous unless it says otherwise, and every door is rate limited per IP unless an API key says who is calling. The three marked costs money call a paid model and need an account.

Every door above also answers under /api/v1/… (/api/v1/optimise and so on) — the same handler, the same limits, the same answer. That is the address to build on: a future incompatible change gets its own /api/v2/… next to it, and /api/v1/… keeps answering exactly as it does today.

MethodPathWhat it is for
POST /api/assistant The cut-list assistant. Sees the page’s table, can change it, cannot spend money. costs money
GET /api/assistant/followup May this customer send their next message without waiting for the answer to the last one? Costs nothing and changes nothing: the page asks once, and a "no" simply means the message box stays locked while the assistant is working, as it always was.
GET /api/assistant/history The signed-in customer’s own past conversations with the assistant: newest first, with the first question as the title.
GET /api/assistant/history/:id One of their own conversations, turn by turn. A conversation opens only when every turn in it belongs to that one address.
GET /api/assistant/jobs/:id Is a long assistant job — reading a drawing, building a cut list — finished yet? Costs nothing: it reads the answer already written, so a connection that drops does not lose the job. A job that is not yours and one that does not exist answer the same way, 404 with no detail, so job numbers cannot be guessed at one by one.
POST /api/assistant/jobs/:id/stop Stop one of your own long assistant jobs when you no longer need it: reading the drawing on would be paying for an answer nobody reads. Rate limited hard — six a minute — because this door changes state rather than reading it.
GET /api/assistant/turn/:id Is the answer to one turn of your own conversation ready? Costs nothing: it reads the answer already written, so a connection that drops mid-answer does not lose it and does not need a second paid call. Takes ?turn=N. A turn that is not yours, and one that does not exist, answer the same as one that is not finished yet.
GET /api/auth/me Who is calling, and whether the AI doors need an account.
POST /api/auth/request Email a sign-in link. No password anywhere in this system.
POST /api/auth/signout End the session.
GET /api/collections The catalogue’s ranges, for the board picker.
GET /api/collections/:handle The boards in one range.
POST /api/enquiry Send this cut list to a human, with attachments. Reaches a phone; rate limited hard.
GET /api/extras/catalogue Search anything else in the catalogue — hinges, runners, glass. Separate from /materials because that one only returns what can be cut: a hinge has no panel size at all. Prices come back, because the price is why you pick the product.
GET /api/health Is it up, is the engine queue busy, how much of today’s paid budget is left.
POST /api/import-program Read a machine file (BPP, MPR, CIX) or a DXF drawing back into a part with machining — the workshop's own old programs, opened in the editor. Free, stores nothing.
POST /api/layout-dxf The same plan as DXF R12 for a CNC router, millimetres, one layer per purpose.
POST /api/layout-pdf The cutting plan as a PDF: three layouts per page, offcuts and machining marked. Send labels: false to print the parts bare — the legend still names them.
GET /api/limits The limits that apply to YOU — parts per calculation, daily AI actions, rate. Comes with the list of what can be configured, so a settings screen is built from the code.
GET /api/machines The machine catalogue behind the export module’s picker — one machine, native format or DXF with its own layers, so the page never grows a button per machine.
POST /api/machining-bpp One machined part as a BPP part program for a Biesse. Refuses anything that would run off the panel.
POST /api/machining-mpr One machined part as an MPR part program for a Homag. Built from sixteen real customer files; refuses the same unsafe geometry as BPP.
GET /api/materials Search the boards we cut. Public catalogue data only.
GET /api/my-boards The customer’s board library: their own boards and the catalogue boards they use, most recent first.
POST /api/my-boards Remember a board in that library, or forget one. Using a board again bumps it up rather than duplicating it.
GET /api/my-orders The customer’s orders in the shop: number, date, total, payment, delivery.
GET /api/my-projects A signed-in customer’s own projects, including everything from the old calculator.
GET /api/my-projects/:id One of the customer’s own projects.
GET /api/my-projects/legacy/:id A project by the number the OLD calculator used — what a years-old email links to.
POST /api/optimise Nest a cut list. The only door that runs the shared engine; everything else works off the plan it returns. Takes an optional mode — see the modes below.
POST /api/order Raise a draft order for the list, with a payment link.
GET /api/projects The projects saved by this browser, or by this account when signed in.
POST /api/projects Save a project; the link it returns is the key to it.
DELETE /api/projects/:token Delete a saved project.
GET /api/projects/:token Open a saved project by its link.
POST /api/projects/claim Attach projects this browser saved before signing in to the account now signed in.
POST /api/quote Price a plan that was already nested — boards, edging, VAT. No geometry is recalculated.
POST /api/read-cut-list Read a photo, PDF or spreadsheet into cut list rows. Costs money per call; behind sign-in. costs money
GET /api/read-quota How many paid AI actions (file readings and assistant messages) you have left today. Free, changes nothing.
POST /api/shaker-door A Shaker front described once - frame width, how many panels, where the rail sits - and answered as ready pockets. Saves drawing dozens of lines by hand.
POST /api/shape-from-cut One straight cut across a part, turned into the part’s own outline. Answers with BOTH halves and their areas — which one is the part is your call, not ours.
POST /api/sheet-bpp One sheet of the plan as a BPP part program: every part cut out by its contour, with its own drilling. One file per sheet — that is how a sheet reaches the machine.
POST /api/sheet-mpr The same sheet program in MPR — the Homag woodWOP format. Same body as sheet-bpp; the writer differs, not the request.
POST /api/sheet-origins-csv The start-points report for one sheet as CSV — where each part’s own zero ended up on the board, and whether it turned. Same numbers sheet-bpp and sheet-mpr write into the machine file, so the printed sheet and the file cannot disagree. Same body as sheet-bpp. Free, no sign-in.
GET /api/sheets Standard UK sheet sizes, and mm↔feet conversion.
POST /api/transcribe A voice recording to text, for the assistant’s input box. costs money
GET /api/v1/health Is it up, is the engine queue busy, how much of today’s paid budget is left.
POST /api/v1/import-program Read a machine file (BPP, MPR, CIX) or a DXF drawing back into a part with machining — the workshop's own old programs, opened in the editor. Free, stores nothing.
POST /api/v1/layout-dxf The same plan as DXF R12 for a CNC router, millimetres, one layer per purpose.
POST /api/v1/layout-pdf The cutting plan as a PDF: three layouts per page, offcuts and machining marked. Send labels: false to print the parts bare — the legend still names them.
GET /api/v1/machines The machine catalogue behind the export module’s picker — one machine, native format or DXF with its own layers, so the page never grows a button per machine.
POST /api/v1/machining-bpp One machined part as a BPP part program for a Biesse. Refuses anything that would run off the panel.
POST /api/v1/machining-mpr One machined part as an MPR part program for a Homag. Built from sixteen real customer files; refuses the same unsafe geometry as BPP.
POST /api/v1/optimise Nest a cut list. The only door that runs the shared engine; everything else works off the plan it returns. Takes an optional mode — see the modes below.
POST /api/v1/quote Price a plan that was already nested — boards, edging, VAT. No geometry is recalculated.
POST /api/v1/shaker-door A Shaker front described once - frame width, how many panels, where the rail sits - and answered as ready pockets. Saves drawing dozens of lines by hand.
POST /api/v1/shape-from-cut One straight cut across a part, turned into the part’s own outline. Answers with BOTH halves and their areas — which one is the part is your call, not ours.
POST /api/v1/sheet-bpp One sheet of the plan as a BPP part program: every part cut out by its contour, with its own drilling. One file per sheet — that is how a sheet reaches the machine.
POST /api/v1/sheet-mpr The same sheet program in MPR — the Homag woodWOP format. Same body as sheet-bpp; the writer differs, not the request.
POST /api/v1/sheet-origins-csv The start-points report for one sheet as CSV — where each part’s own zero ended up on the board, and whether it turned. Same numbers sheet-bpp and sheet-mpr write into the machine file, so the printed sheet and the file cannot disagree. Same body as sheet-bpp. Free, no sign-in.
GET /api/v1/sheets Standard UK sheet sizes, and mm↔feet conversion.
GET /auth The link from the sign-in email lands here: it swaps the token for a cookie and sends you back to the calculator.
GET /staff A one-time handoff from the authenticated Uniboards admin. It starts an administrator session and selects the requested project without becoming that customer.

The public doors, one by one

Every request below was posted to a running server and every answer is what came back, captured on 1 September 2026. Nothing here is written out by hand from memory.

POST /api/optimise

Nest a cut list. The only door that runs the shared engine; everything else works off the plan it returns. Takes an optional mode — see the modes below.

Required

  • materials[] — at least one, and no more than the material limit below.
  • materials[].panel{ width, length } of the board, in millimetres.
  • materials[].parts[] — at least one part per material.
  • parts[].length, parts[].width — the finished size in millimetres. Length runs along the grain.

Optional

  • parts[].qty — how many of this part. Default 1.
  • parts[].name — echoed back on every copy as name (1), name (2)
  • parts[].can_rotatefalse locks this part to the grain.
  • parts[].bevel — for a part with one straight angled edge — a corner door, a panel under a rake: { corner, length, width }. corner is BL/BR/TL/TR in the part's own axes (x along the length), and the two numbers say how much of the length and of the width the cut eats from that corner. Two identical bevelled parts are packed as ONE rectangle, so an angled list can need half the boards; the answer still places each part separately, in its real position.
  • materials[].ref — your own id for the board. Echoed back, and the only thing the pricing door matches on.
  • materials[].title — a readable name. Echoed back on the plan; never sent to the engine.
  • materials[].can_rotatefalse for a board with a grain or a pattern direction.
  • mode — one of the cutting modes below. Default guillotine. An unknown value falls back to the default rather than failing.
  • params.spacing — saw kerf between parts, mm. Default 4.5.
  • params.edge_spacing — trim off the board edge, mm. Default 0.
  • params.small_edge_guard_mm — keep small parts at least this far from the board edge, mm. Default 0 (off) — a small part right on the edge chips on the saw. The answer says whether it was applied (summary.small_edge_guard_applied).
  • materials[].stock[] — offcuts you already have: { length, width, qty } in millimetres. They are cut from first, and the answer says how many sheets came off the rack (summary.from_stock) and how many must be bought (summary.to_buy). A sheet cut from an offcut carries from_stock: true and its own panel.

Request

{
  "materials": [
    {
      "ref": "mdf18",
      "panel": { "width": 1220, "length": 2440 },
      "parts": [{ "name": "shelf", "length": 600, "width": 400, "qty": 4 }]
    }
  ]
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/optimise' \
  -H 'Content-Type: application/json' \
  -d '{
  "materials": [
    {
      "ref": "mdf18",
      "panel": { "width": 1220, "length": 2440 },
      "parts": [{ "name": "shelf", "length": 600, "width": 400, "qty": 4 }]
    }
  ]
}'

Answer

{
  "ok": true,
  "engine": { "version": "V40", "algorithm": "guillotine", "ms": 0.4 },
  "summary": {
    "boards_total": 1,
    "parts_placed": 4,
    "parts_requested": 4,
    "unplaced": 0,
    "average_occupancy_pct": 32.249,
    "cuttable_on_saw": true,
    "offcuts": 1,
    "offcut_area_mm2": 1969578,
    "largest_offcut_mm2": 1969578,
    "scrap_area_mm2": 47320,
    "free_area_mm2": 2016898,
    "materials": [
      {
        "ref": "mdf18",
        "title": null,
        "can_rotate": true,
        "panel": { "width": 1220, "length": 2440 },
        "boards": 1,
        "occupancy_pct": 32.249,
        "sheets": [
          {
            "board": 1,
            "occupancy_pct": 32.249,
            "waste_pct": 67.8,
            "offcuts": 1,
            "offcut_area_mm2": 1969578,
            "largest_offcut_mm2": 1969578,
            "scrap_area_mm2": 47320,
            "free_area_mm2": 2016898,
            "parts": [
              {
                "name": "shelf (1)",
                "part_id": "0_0",
                "number": 1,
                "x": 0,
                "y": 0,
                "width": 600,
                "height": 400,
                "rotated": true
              },
              {
                "name": "shelf (2)",
                "part_id": "0_0",
                "number": 2,
                "x": 0,
                "y": 404.5,
                "width": 600,
                "height": 400,
                "rotated": true
              },
              {
                "name": "shelf (3)",
                "part_id": "0_0",
                "number": 3,
                "x": 0,
                "y": 809,
                "width": 600,
                "height": 400,
                "rotated": true
              },
              {
                "name": "shelf (4)",
                "part_id": "0_0",
                "number": 4,
                "x": 0,
                "y": 1213.5,
                "width": 600,
                "height": 400,
                "rotated": true
              }
            ]
          }
        ]
      }
    ]
  }
}

engine.ms is how long the engine took on that call, so it differs every time.

Refusals

StatuscodeWhat it means
422invalid_inputThe body is not a cut list: materials must be a non-empty array., materials[0].panel needs width and length in mm, both above zero.
422invalid_inputOver a limit: Too many parts: 5000. This free door allows 200. Send us the list and we will run it in full.
422invalid_inputThe engine refused the geometry: part does not fit the panel.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.
503busyThe engine queue is full. Retry-After: 5 — retry, do not give up.
504timeoutThe engine did not answer in time. The plan is not half-made; nothing was kept.
502The engine answered with something we cannot use. Not your request.

Free and unmetered beyond the rate limit. Body up to 512kb; shares the optimise request budget.

POST /api/quote

Price a plan that was already nested — boards, edging, VAT. No geometry is recalculated.

Required

  • materials[] — at least one.
  • materials[].ref — the catalogue reference. Get one from GET /api/materials?q=; a ref that is not a catalogue product comes back with no price rather than an error.
  • materials[].sheets — how many boards of it — the boards the plan returned.

Optional

  • materials[].panel — the board size. Without it the cutting labour cannot be worked out and a warning says so.
  • materials[].title — a readable name, used in the warnings.

Request

{ "materials": [{ "ref": "41948204007593", "panel": { "width": 1220, "length": 2440 }, "sheets": 2 }] }
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/quote' \
  -H 'Content-Type: application/json' \
  -d '{ "materials": [{ "ref": "41948204007593", "panel": { "width": 1220, "length": 2440 }, "sheets": 2 }] }'

Answer

{
  "ok": true,
  "quote": {
    "currency": "GBP",
    "lines": [
      {
        "kind": "board",
        "material_ref": "41948204007593",
        "label": "FINSA Hidrofugo Moisture Resistant MDF 1220x2440 8'x4' - 18mm",
        "product_title": "FINSA Hidrofugo Moisture Resistant MDF 1220x2440 8'x4'",
        "variant_title": "18mm",
        "image_url": "https://cdn.shopify.com/s/files/1/0541/7071/4281/files/FinsaHidrofugo1.jpg",
        "sku": "Finsa Hidrofugo MR MDF 8x4 18mm",
        "qty": 2,
        "unit_price": 54.97,
        "amount": 109.94,
        "edged": false
      },
      {
        "kind": "cutting",
        "code": "cutting_service",
        "covers": ["41948204007593"],
        "tier": "8x4",
        "service": "cut_only",
        "label": "Cutting service — 8'x4' Cut only",
        "qty": 2,
        "unit_price": 12,
        "amount": 24,
        "note": "2 boards × £12"
      }
    ],
    "subtotal_ex_vat": 133.94,
    "vat_rate": 0.2,
    "vat_amount": 26.79,
    "total_inc_vat": 160.73,
    "excludes": ["Delivery not included — quoted separately."],
    "warnings": [],
    "complete": true,
    "priced_at": "2026-09-01T12:13:40.171Z",
    "rules_snapshot": "… trimmed here; the real answer carries the pricing rules that produced these lines …"
  }
}

Prices are the shop's live catalogue prices, so these numbers are what the board cost on the day this page was written. complete: false plus a warnings entry means part of the total is missing — show the warning, do not hide it. The real answer also carries the whole rules_snapshot — the pricing rules that produced these lines — and the real image_url has a cache-busting suffix; both are shortened above for length.

Refusals

StatuscodeWhat it means
422invalid_inputmaterials must be a non-empty array.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.
503not_configuredThis site has no catalogue behind it, so nothing can be priced.
504timeoutThe catalogue did not answer in time.
502The catalogue refused. Not your request.

Free and unmetered beyond the rate limit. Body up to 512kb; shares the quote request budget.

POST /api/layout-pdf

The cutting plan as a PDF: three layouts per page, offcuts and machining marked. Send labels: false to print the parts bare — the legend still names them.

Required

  • materials[] — the plan's own summary.materials, handed straight back.
  • materials[].panel, materials[].sheets[].parts[] — each part with x, y, width, height.

Optional

  • labelsfalse prints the parts bare — no name written inside the rectangle. The legend still names them.
  • materials[].title — the heading above each board. Falls back to ref, then to "Board".

Request

This is { "materials": … } taken verbatim from the answer above. Nothing is nested a second time.

{
  "materials": [
    {
      "ref": "mdf18",
      "title": null,
      "can_rotate": true,
      "panel": { "width": 1220, "length": 2440 },
      "boards": 1,
      "occupancy_pct": 32.249,
      "sheets": [
        {
          "board": 1,
          "occupancy_pct": 32.249,
          "waste_pct": 67.8,
          "offcuts": 1,
          "offcut_area_mm2": 1969578,
          "largest_offcut_mm2": 1969578,
          "scrap_area_mm2": 47320,
          "free_area_mm2": 2016898,
          "parts": [
            {
              "name": "shelf (1)",
              "part_id": "0_0",
              "number": 1,
              "x": 0,
              "y": 0,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (2)",
              "part_id": "0_0",
              "number": 2,
              "x": 0,
              "y": 404.5,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (3)",
              "part_id": "0_0",
              "number": 3,
              "x": 0,
              "y": 809,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (4)",
              "part_id": "0_0",
              "number": 4,
              "x": 0,
              "y": 1213.5,
              "width": 600,
              "height": 400,
              "rotated": true
            }
          ]
        }
      ]
    }
  ]
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/layout-pdf' \
  -o cut-plan.pdf \
  -H 'Content-Type: application/json' \
  -d '{
  "materials": [
    {
      "ref": "mdf18",
      "title": null,
      "can_rotate": true,
      "panel": { "width": 1220, "length": 2440 },
      "boards": 1,
      "occupancy_pct": 32.249,
      "sheets": [
        {
          "board": 1,
          "occupancy_pct": 32.249,
          "waste_pct": 67.8,
          "offcuts": 1,
          "offcut_area_mm2": 1969578,
          "largest_offcut_mm2": 1969578,
          "scrap_area_mm2": 47320,
          "free_area_mm2": 2016898,
          "parts": [
            {
              "name": "shelf (1)",
              "part_id": "0_0",
              "number": 1,
              "x": 0,
              "y": 0,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (2)",
              "part_id": "0_0",
              "number": 2,
              "x": 0,
              "y": 404.5,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (3)",
              "part_id": "0_0",
              "number": 3,
              "x": 0,
              "y": 809,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (4)",
              "part_id": "0_0",
              "number": 4,
              "x": 0,
              "y": 1213.5,
              "width": 600,
              "height": 400,
              "rotated": true
            }
          ]
        }
      ]
    }
  ]
}'

Answer

A file, not JSON:

Content-Type: application/pdf
Content-Disposition: attachment; filename="cut-plan.pdf"

A4 PDF, three layouts per page, beginning %PDF-1.3. The one-board plan above came back just under 2 kB (the exact size moves: a PDF carries its own creation date).

Refusals

StatuscodeWhat it means
422invalid_inputmaterials must be a non-empty array., or a board with no usable size.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 1mb; shares the layout-pdf request budget.

POST /api/layout-dxf

The same plan as DXF R12 for a CNC router, millimetres, one layer per purpose.

Required

  • materials[] — exactly the body /api/layout-pdf takes.

Optional

  • layers — what to call the layers, when your machine expects its own names: { board, parts, text, machining, blank }. Anything you leave out keeps our name. Spaces and punctuation become underscores — a layer name in R12 cannot carry them.

Request

{
  "materials": [
    {
      "ref": "mdf18",
      "title": null,
      "can_rotate": true,
      "panel": { "width": 1220, "length": 2440 },
      "boards": 1,
      "occupancy_pct": 32.249,
      "sheets": [
        {
          "board": 1,
          "occupancy_pct": 32.249,
          "waste_pct": 67.8,
          "offcuts": 1,
          "offcut_area_mm2": 1969578,
          "largest_offcut_mm2": 1969578,
          "scrap_area_mm2": 47320,
          "free_area_mm2": 2016898,
          "parts": [
            {
              "name": "shelf (1)",
              "part_id": "0_0",
              "number": 1,
              "x": 0,
              "y": 0,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (2)",
              "part_id": "0_0",
              "number": 2,
              "x": 0,
              "y": 404.5,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (3)",
              "part_id": "0_0",
              "number": 3,
              "x": 0,
              "y": 809,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (4)",
              "part_id": "0_0",
              "number": 4,
              "x": 0,
              "y": 1213.5,
              "width": 600,
              "height": 400,
              "rotated": true
            }
          ]
        }
      ]
    }
  ]
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/layout-dxf' \
  -o cut-plan.dxf \
  -H 'Content-Type: application/json' \
  -d '{
  "materials": [
    {
      "ref": "mdf18",
      "title": null,
      "can_rotate": true,
      "panel": { "width": 1220, "length": 2440 },
      "boards": 1,
      "occupancy_pct": 32.249,
      "sheets": [
        {
          "board": 1,
          "occupancy_pct": 32.249,
          "waste_pct": 67.8,
          "offcuts": 1,
          "offcut_area_mm2": 1969578,
          "largest_offcut_mm2": 1969578,
          "scrap_area_mm2": 47320,
          "free_area_mm2": 2016898,
          "parts": [
            {
              "name": "shelf (1)",
              "part_id": "0_0",
              "number": 1,
              "x": 0,
              "y": 0,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (2)",
              "part_id": "0_0",
              "number": 2,
              "x": 0,
              "y": 404.5,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (3)",
              "part_id": "0_0",
              "number": 3,
              "x": 0,
              "y": 809,
              "width": 600,
              "height": 400,
              "rotated": true
            },
            {
              "name": "shelf (4)",
              "part_id": "0_0",
              "number": 4,
              "x": 0,
              "y": 1213.5,
              "width": 600,
              "height": 400,
              "rotated": true
            }
          ]
        }
      ]
    }
  ]
}'

Answer

A file, not JSON:

Content-Type: application/dxf
Content-Disposition: attachment; filename="cut-plan.dxf"

DXF R12, plain text, millimetres ($INSUNITS 4). Layers: BOARD, PARTS, TEXT, MACHINING. The one-board plan above came back as 489 lines / 1,817 bytes. Several boards are laid out side by side in one drawing.

Refusals

StatuscodeWhat it means
422invalid_inputNothing to export — calculate a plan first., or A material has no usable panel size.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 1mb; shares the layout-pdf request budget.

POST /api/machining-bpp

One machined part as a BPP part program for a Biesse. Refuses anything that would run off the panel.

Required

  • part.length, part.width — the part, in millimetres.
  • part.machining[] — at least one operation. A part with none is refused: an empty program is not a file anyone wants on a machine.

Optional

  • part.name — becomes the file name, cut down to letters, digits, - and _.
  • part.thickness — the board thickness. Defaults to the setting below.
  • part.stock{ length, width } of the piece actually on the machine, when it is bigger than the part. The part is centred in it and the outline is cut.
  • part.variables — the program's own variables — [{ name, value }], the way BiesseWorks keeps them. A value is a number (29.8) or a formula of names, numbers, + - * / and brackets. LPX, LPY and LPZ are the panel this file declares, so LPY + 40 counts from the real board. Any numeric field of any operation may name a variable instead of a number; the values are worked out before the file is written. An unknown name, a circle or a division by zero is refused with words rather than written as a number nobody meant.
  • settings — the workshop's own machine settings — see the block under this table. They travel in the request because this service stores nothing about anybody.

Request

Operations are kind: "hole", "holes-row", "groove", "path", "edge-holes" (MPR only — no BPP sample has a horizontal drilling line to copy, so BPP refuses it rather than inventing one), and anything else is treated as a hinge cup, as above.

{
  "part": {
    "name": "door",
    "length": 800,
    "width": 400,
    "thickness": 18,
    "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 700] }]
  }
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/machining-bpp' \
  -o door.bpp \
  -H 'Content-Type: application/json' \
  -d '{
  "part": {
    "name": "door",
    "length": 800,
    "width": 400,
    "thickness": 18,
    "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 700] }]
  }
}'

Answer

A file, not JSON:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="door.bpp"

A BiesseWorks part program, VER=150 by default, CRLF. The part above came back as 53 lines / 1,158 bytes, starting:

[HEADER]
TYPE=BPP
VER=150

[DESCRIPTION]
TEST FILE - not verified on a machine. Dry-run first. |

[VARIABLES]
PAN=LPX|800||4|
PAN=LPY|400||4|
PAN=LPZ|18||4|

Refusals

StatuscodeWhat it means
422invalid_inputThe part has no size., That part has no machining on it — a BPP would be an empty program., or an operation that would run off the panel.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 512kb; shares the layout-pdf request budget.

POST /api/machining-mpr

One machined part as an MPR part program for a Homag. Built from sixteen real customer files; refuses the same unsafe geometry as BPP.

Required

  • part — exactly the body /api/machining-bpp takes.

Optional

  • settings — the same settings block. Fields the format has no confirmed place for are ignored, not guessed at.

Request

{
  "part": {
    "name": "door",
    "length": 800,
    "width": 400,
    "thickness": 18,
    "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 700] }]
  }
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/machining-mpr' \
  -o door.mpr \
  -H 'Content-Type: application/json' \
  -d '{
  "part": {
    "name": "door",
    "length": 800,
    "width": 400,
    "thickness": 18,
    "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 700] }]
  }
}'

Answer

A file, not JSON:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="door.mpr"

A Homag woodWOP program. The same part came back as 142 lines / 1,593 bytes, starting:

[H
VERSION="4.0 Alpha"
WW="6.0.62"
OP="1"

Refusals

StatuscodeWhat it means
422invalid_inputSame refusals as BPP, plus anything MPR itself cannot carry.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 512kb; shares the layout-pdf request budget.

POST /api/sheet-bpp

One sheet of the plan as a BPP part program: every part cut out by its contour, with its own drilling. One file per sheet — that is how a sheet reaches the machine.

Required

  • material.panel{ length, width } of the board, and thickness if it is not 18.
  • sheet.parts[] — the parts on this one board, with x, y, width, height — the plan's own numbers.

Optional

  • material.title — the file name. Falls back to ref, then to "board".
  • sheet.board — the board number, which goes in the file name. Default 1.
  • sheet.parts[].machining — per part, the same operations /api/machining-bpp takes. They move with the part.
  • sheet.parts[].variables — per part, the same variables /api/machining-bpp takes. Here LPX and LPY are the BOARD, because the panel on the machine is the board.
  • sheet.parts[].ordered — the size as ordered, written into the file for the operator.
  • settings — the same machine settings block.

Request

One board per call. A plan with four boards is four calls and four files — that is how a sheet reaches the machine.

{
  "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
  "sheet": {
    "board": 1,
    "parts": [
      {
        "x": 0,
        "y": 0,
        "width": 600,
        "height": 400,
        "ordered": "600 x 400",
        "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 500] }]
      }
    ]
  }
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/sheet-bpp' \
  -o MDF_18_sheet-1.bpp \
  -H 'Content-Type: application/json' \
  -d '{
  "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
  "sheet": {
    "board": 1,
    "parts": [
      {
        "x": 0,
        "y": 0,
        "width": 600,
        "height": 400,
        "ordered": "600 x 400",
        "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 500] }]
      }
    ]
  }
}'

Answer

A file, not JSON:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="MDF_18_sheet-1.bpp"

The sheet above came back as 60 lines / 1,900 bytes. The panel in the file is the BOARD (LPX 2440, LPY 1220) and each part is cut out of it by its own contour.

Refusals

StatuscodeWhat it means
422invalid_inputA board with no size, or an operation that would run off it.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 1mb; shares the layout-pdf request budget.

POST /api/sheet-mpr

The same sheet program in MPR — the Homag woodWOP format. Same body as sheet-bpp; the writer differs, not the request.

Required

  • material, sheet — exactly the body /api/sheet-bpp takes.

Optional

  • settings — the same machine settings block.

Request

{
  "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
  "sheet": {
    "board": 1,
    "parts": [
      {
        "x": 0,
        "y": 0,
        "width": 600,
        "height": 400,
        "ordered": "600 x 400",
        "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 500] }]
      }
    ]
  }
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/sheet-mpr' \
  -o MDF_18_sheet-1.mpr \
  -H 'Content-Type: application/json' \
  -d '{
  "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
  "sheet": {
    "board": 1,
    "parts": [
      {
        "x": 0,
        "y": 0,
        "width": 600,
        "height": 400,
        "ordered": "600 x 400",
        "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 500] }]
      }
    ]
  }
}'

Answer

A file, not JSON:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="MDF_18_sheet-1.mpr"

The same sheet came back as 243 lines / 2,552 bytes.

Refusals

StatuscodeWhat it means
422invalid_inputSame refusals as the BPP sheet.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 1mb; shares the layout-pdf request budget.

POST /api/sheet-origins-csv

The start-points report for one sheet as CSV — where each part’s own zero ended up on the board, and whether it turned. Same numbers sheet-bpp and sheet-mpr write into the machine file, so the printed sheet and the file cannot disagree. Same body as sheet-bpp. Free, no sign-in.

Required

  • material, sheet — exactly the body /api/sheet-bpp takes.

Optional

  • settings — the same machine settings block — the report reads the same zero corner as the file does.

Request

One board per call, same as sheet-bpp and sheet-mpr. Free and no sign-in: nothing here is nested again, it only writes out numbers /api/sheet-bpp already had to work out.

{
  "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
  "sheet": {
    "board": 1,
    "parts": [
      {
        "x": 0,
        "y": 0,
        "width": 600,
        "height": 400,
        "ordered": "600 x 400",
        "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 500] }]
      }
    ]
  }
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/sheet-origins-csv' \
  -o start-points-MDF_18-sheet-1.csv \
  -H 'Content-Type: application/json' \
  -d '{
  "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
  "sheet": {
    "board": 1,
    "parts": [
      {
        "x": 0,
        "y": 0,
        "width": 600,
        "height": 400,
        "ordered": "600 x 400",
        "machining": [{ "kind": "hinge-screw", "edge": "L1", "inset": 22, "positions": [100, 500] }]
      }
    ]
  }
}'

Answer

A file, not JSON:

Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="start-points-MDF_18-sheet-1.csv"

One header row plus one row per part: <code>#,part,size,x,y,turned_deg,ops</code>. <code>x,y</code> is where that part’s own zero corner sits on the BOARD, measured from the corner <code>settings.origin</code> names (default bottom-left) — the same numbers written into the sheet’s BPP/MPR file. <code>turned_deg</code> is 90 when the part lies across the board, 0 otherwise. The sheet above came back as 2 lines.

Refusals

StatuscodeWhat it means
422invalid_inputA board with no size, or a machining variable that will not resolve.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 1mb; shares the layout-pdf request budget.

POST /api/import-program

Read a machine file (BPP, MPR, CIX) or a DXF drawing back into a part with machining — the workshop's own old programs, opened in the editor. Free, stores nothing.

Required

  • text — the whole machine file as text. Read it as latin1 — these files are not UTF-8.

Optional

  • name — the file name. Only used to break a tie when the first line does not say which format it is.

Request

The body is { "text": "…the whole file…", "name": "door.bpp" }. Read the file as latin1, not UTF-8. The answer below is our own BPP for the door above, posted straight back.

jq -Rs '{ name: "door.bpp", text: . }' door.bpp \
  | curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/import-program' \
      -H 'Content-Type: application/json' --data-binary @-

Answer

{
  "ok": true,
  "format": "BPP (VER=150)",
  "part": {
    "name": "door",
    "length": 800,
    "width": 400,
    "thickness": 18,
    "crn": "2",
    "crnBase": "2",
    "machining": [
      {
        "kind": "hole",
        "corner": "BL",
        "x": 100,
        "y": 22,
        "diameter": 35,
        "depth": 12.5,
        "through": false,
        "side": "B",
        "tooltype": "NORMALE",
        "repeat": 1,
        "stepX": 0,
        "stepY": 0
      },
      {
        "kind": "hole",
        "corner": "BL",
        "x": 700,
        "y": 22,
        "diameter": 35,
        "depth": 12.5,
        "through": false,
        "side": "B",
        "tooltype": "NORMALE",
        "repeat": 1,
        "stepX": 0,
        "stepY": 0
      }
    ]
  },
  "parts": [
    {
      "name": "door",
      "length": 800,
      "width": 400,
      "thickness": 18,
      "crn": "2",
      "crnBase": "2",
      "machining": [
        {
          "kind": "hole",
          "corner": "BL",
          "x": 100,
          "y": 22,
          "diameter": 35,
          "depth": 12.5,
          "through": false,
          "side": "B",
          "tooltype": "NORMALE",
          "repeat": 1,
          "stepX": 0,
          "stepY": 0
        },
        {
          "kind": "hole",
          "corner": "BL",
          "x": 700,
          "y": 22,
          "diameter": 35,
          "depth": 12.5,
          "through": false,
          "side": "B",
          "tooltype": "NORMALE",
          "repeat": 1,
          "stepX": 0,
          "stepY": 0
        }
      ]
    }
  ],
  "notes": []
}

A file that turns out to be a whole SHEET comes back with parts[] as well — one entry per part cut out of it. notes[] is where the reader says what it could not carry over (a pocket it has no operation for, a stock bigger than the part), in plain English, meant to be shown to the person who opened the file.

Refusals

StatuscodeWhat it means
422invalid_inputThat file is empty., This MPR has no part size in it.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 2mb; shares the layout-pdf request budget.

POST /api/shape-from-cut

One straight cut across a part, turned into the part’s own outline. Answers with BOTH halves and their areas — which one is the part is your call, not ours.

Required

  • from — where the cut meets the outline, [x, y] in mm from the bottom-left corner of the part.
  • to — where it leaves the outline, [x, y].

Optional

  • length — length of the rectangle the part is cut from, mm. Give length and width, or give contour.
  • width — width of that rectangle, mm.
  • contour — the part outline as points, when it is not a rectangle: [{x, y}, ...].

Request

{ "length": 485, "width": 200, "from": [255, 200], "to": [485, 0] }
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/shape-from-cut' \
  -H 'Content-Type: application/json' \
  -d '{ "length": 485, "width": 200, "from": [255, 200], "to": [485, 0] }'

Answer

{
  "ok": true,
  "pieces": [
    {
      "points": [{ "x": 255, "y": 200 }, { "x": 0, "y": 200 }, { "x": 0, "y": 0 }, { "x": 485, "y": 0 }],
      "area": 74000
    },
    { "points": [{ "x": 485, "y": 0 }, { "x": 485, "y": 200 }, { "x": 255, "y": 200 }], "area": 23000 }
  ],
  "note": "Which piece is the part is not decided here — pick one and set it as the part contour. Then the outline is cut in one pass and the offcut never comes loose under the cutter."
}

Areas are in mm². They add up to the rectangle they came from — 74000 + 23000 = 485 × 200.

Refusals

StatuscodeWhat it means
422invalid_inputthe cut starts at …, which is not on the outline. A cut that shapes the part has to run edge to edge. — a cut begun inside the part does not separate anything. Also both ends sit on the same side, so the cut takes nothing off., a cut from a point to itself, and a part with no outline.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 256kb; shares the optimise request budget.

POST /api/shaker-door

A Shaker front described once - frame width, how many panels, where the rail sits - and answered as ready pockets. Saves drawing dozens of lines by hand.

Required

  • length — height of the door, mm - the way it hangs.
  • width — width of the door, mm.

Optional

  • frame — width of the outer frame on all four sides, mm. Default 80 - "usually 80 or 90".
  • panels — how many panels: 1 for a drawer front, 2 for a tall door with a rail, 3-4 for classics. Default 1.
  • rail_at — height of the rail above the bottom, mm. Default 900 - worktop level, so rails line up across a run. Only for 2 panels.
  • rail — thickness of the rail between panels, mm. Defaults to the frame width.
  • depth — how deep the panels are milled, mm. Default 6.
  • radius — corner radius of each panel, mm - the radius of your finishing cutter.
  • overlap — how much the milling passes overlap, per cent. Default 80.

Request

{ "length": 2000, "width": 500, "frame": 80, "panels": 2, "depth": 6, "radius": 8 }
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/shaker-door' \
  -H 'Content-Type: application/json' \
  -d '{ "length": 2000, "width": 500, "frame": 80, "panels": 2, "depth": 6, "radius": 8 }'

Answer

{
  "ok": true,
  "pockets": [
    {
      "kind": "pocket",
      "id": "shaker-1",
      "x": 470,
      "y": 250,
      "length": 780,
      "width": 340,
      "radius": 8,
      "depth": 6,
      "overlap": 80
    },
    {
      "kind": "pocket",
      "id": "shaker-2",
      "x": 1430,
      "y": 250,
      "length": 980,
      "width": 340,
      "radius": 8,
      "depth": 6,
      "overlap": 80
    }
  ],
  "note": "Put these on the part as machining. Each one goes to a Homag as a single Tasche macro and to a Biesse as concentric passes, because BiesseWorks has no pocket of its own."
}

The rail sits at 900 mm above the bottom by default, so the lower panel is shorter than the upper one.

Refusals

StatuscodeWhat it means
422invalid_inputa 80 mm frame on both sides leaves nothing of a 150 mm door., or a rail that does not fit between the frames.
429rate_limitedToo many requests from this address. Retry-After says how many seconds to wait.

Free and unmetered beyond the rate limit. Body up to 256kb; shares the optimise request budget.

GET /api/sheets

Standard UK sheet sizes, and mm↔feet conversion.

Required

None.

Optional

  • (nothing) — the whole table of standard UK sheet sizes.
  • ?width=&length= — describe one board: its name, area and imperial size.
  • ?mm= — millimetres to feet and inches.
  • ?ft= — feet to millimetres.

Request

curl -s 'https://cut.uniboards.co.uk/v2/api/sheets?mm=2440'

Answer

{ "ok": true, "converted": { "mm": 2440, "inches": 96.06, "feet": 8.005, "text": "8 ft 0.06 in" } }

With no query it answers { "ok": true, "sheets": [ … ] } — six boards, each with width, length, name, note, area_m2 and its imperial equivalent. 8x4 is 1220 × 2440 (2.977 m²); Egger MFC is 2070 × 2800, which is the size most cut lists get wrong.

Refusals

StatuscodeWhat it means
This door has no failure mode: a nonsense query gets the whole table.

No engine, no upstream, no rate limit worth enforcing — the answer is a constant.

GET /api/health

Is it up, is the engine queue busy, how much of today’s paid budget is left.

Required

None.

Optional

None.

Request

curl -s 'https://cut.uniboards.co.uk/v2/api/health'

Answer

{
  "ok": true,
  "gate": { "running": 0, "waiting": 0, "concurrency": 2 },
  "vision_used_today": 0,
  "vision_budget_today": 200
}

gate is the engine queue right now: how many nests are running, how many are waiting, and how many may run at once. waiting climbing towards the queue limit is why a call comes back busy. The two vision_ numbers are today's paid budget for the whole service, not yours.

Refusals

StatuscodeWhat it means
It answers 200 or it is down.

Free, unlimited, and it says nothing about who called.

Machine settings

The four machine-program doors take an optional settings object. It travels in the request rather than living on the server because this service keeps nothing about anybody, and tool numbers are exactly what two workshops will never agree on. Everything has a default, so a request with no settings still produces a file.

  • version — 150 (default) or 120, the two BiesseWorks dialects. BPP only.
  • thickness — the board, mm. Default 18, overridden by part.thickness.
  • cupDiameter, cupDepth — the hinge cup. Default 35 mm × 12.5 mm, which is a Blum cup in an 18 mm door.
  • boreTool, cupTool, routTool — tool codes for VER=120, which names its tools in the file.
  • cutToolName, cutSpeed, cutDiameter — the cutter that takes the part out of the board. Defaults 8MMCOMPR, 6000 mm/min, 8 mm.
  • cutCorrectionauto (default), left, right or central: which side of the line the cutter runs. auto works it out from the contour; a named side that disagrees with the contour is refused with the millimetres the part would lose. It is a check, not a field: no sample file confirms where (CRC) sits in a BPP milling row, so it is not written into one — in MPR it is, as RK.
  • crn"1" (default) to "4": the reference corner of a BPP milling row, the four-corner picture inside the operation. It is not the panel zero (origin) — it mirrors Y for that one operation, so changing it moves the toolpath.
  • cutLeadIn, cutLeadOut, cutLeadAngle — how the cutter enters and leaves. auto by default.
  • origin — where the machine's zero corner is: BL (default), BR, TL, TR. It moves every coordinate in the file and nothing else.
  • offsetX, offsetY — shift every coordinate, mm.
Cutting modes

The mode on /api/optimise chooses how the parts are packed. The engine's own algorithm names never leave the service — these are the whole list, and an unknown value falls back to the default rather than failing the job.

  • guillotine default — straight cuts right across the board, in the order a panel saw makes them. What most workshops can actually cut.
  • nesting — the engine's tightest packing. Fewer boards, but a part can end up surrounded by others — a CNC router job, not a saw one.
  • nesting-small-inside — nesting, with the narrow parts (any side under 200 mm) tucked into the gaps between the big ones instead of taking a board edge each.
The shape of an answer

Success is { "ok": true, … } and failure is { "ok": false, "error": { "code", "message" } } — the message is written to be shown to a person as it is. A door that returns a file returns that file on success and this same JSON shape on failure, so always check the status before treating the body as a download.

StatuscodeWhat it means, and what to do
422invalid_inputYour request. The message says which field and why — show it. Nothing was run.
401signin_requiredOnly the three paid doors, marked in the table above. No public door on this page can return it.
413too_largeThe body is over that door's size limit. Split the request.
429rate_limitedToo many requests from your address. Retry-After is in seconds; the message says the same in English.
503busyThe engine queue is full. Retry-After: 5. This is temporary and worth retrying — it is not a failure of your request.
503not_configuredThat door needs an upstream this site does not have (pricing on a site with no shop behind it).
504timeoutThe engine or the catalogue did not answer in time. Nothing half-done was kept.
502variousAn upstream answered with something unusable. Not your request; retrying later is reasonable.
500internalOur fault. It is logged with no cut list in it.

The limits, honestly

What is free, and what is not

Free, unmetered, no account: the calculation itself (/api/optimise), the PDF, the DXF, every machine program (BPP and MPR, part and sheet), reading a machine file back in, the sheet-size table and the health check. There is no daily quota on any of them and no per-day counter is even kept — only the per-minute rate below.

Costs real money per call, and is therefore counted per day: POST /api/read-cut-list, POST /api/assistant, POST /api/transcribe. These call a paid model, need an account (free, by email link, no password), and are capped at 500 actions a day once you are signed in (30 without an account), plus a ceiling of 10000 for the whole service per day. GET /api/health shows how much of today's service-wide budget is gone.

Nothing on this page requires an account or a key to use it once. If you need more than the numbers below, ask — that is a cheaper conversation than a workaround, and it is also how an API key below gets issued.

API keys — for a system, not a person

Every door works with no key at all, exactly as documented above — the limits are then per IP address, as they always were. A key exists for the opposite case: a system that calls this API on behalf of many people (an agent, a dispatcher) and needs its own quota instead of sharing one IP's budget with everyone else behind the same address, or with a stranger on the same network.

Ask for one — none are issued automatically. Send it as x-api-key: <your key> on any request, including the plain /api/… address; it does not require /api/v1/… and /api/v1/… does not require it. Only the hash of a key is ever kept on this server, the same way a sign-in link is: a leaked copy of its configuration is a list of useless hashes, not a list of working keys.

Three different answers, never one blanket "access denied":

SituationWhat happens
No x-api-key header at allAnonymous, exactly as above — not a refusal.
Header present, key not recognised401 invalid_api_key — a typo or a revoked key must not look like a working one with less quota.
Header present, key recognised, over its own rate or size limitThe same 429 rate_limited / 422 invalid_input as everyone else, just against the key's own numbers below rather than the IP's.

A key does not, by itself, open POST /api/assistant or the other AI doors below — those sit behind a sign-in the core checks by session, and a calculator key is not a session. Building that bridge honestly is separate work, not done here.

How much you can send at once

The engine itself accepts far more, but it shares its workers with a live shop, so the public number is deliberately below its ceiling. Signing in raises it; it costs nothing and takes an email link.

You areParts per calculationMaterials per calculationPaid AI actions a day
anon200630
signed_in100018500
api_key100018500
premium2000362000
admin100007210000

Over the limit is a 422 naming the number, not a truncated answer. Sizes are per REQUEST — a 900-part list is three calls, and each is a separate plan.

How often you can call

12 requests a minute per address, with a burst of 20. Every answer carries X-RateLimit-Remaining; going over is a 429 with Retry-After in seconds. Budgets are per bucket, and several doors share one on purpose — the page calls them in a row on the same plan, and separate buckets would mean four times as many requests get through as the single number suggests.

DoorRequest budgetBody up to
POST /api/optimiseoptimise512kb
POST /api/quotequote512kb
POST /api/layout-pdflayout-pdf1mb
POST /api/layout-dxflayout-pdf1mb
POST /api/machining-bpplayout-pdf512kb
POST /api/machining-mprlayout-pdf512kb
POST /api/sheet-bpplayout-pdf1mb
POST /api/sheet-mprlayout-pdf1mb
POST /api/sheet-origins-csvlayout-pdf1mb
POST /api/import-programlayout-pdf2mb
POST /api/shape-from-cutoptimise256kb
POST /api/shaker-dooroptimise256kb

2 calculations run at once across every caller in the world, with room for 20 more waiting up to 15 seconds. Past that it is 503 busy with Retry-After: 5 rather than a queue nobody comes back to. Everything after the calculation — PDF, DXF, machine programs — is done in this process and never touches that queue.

MCP: the same tools, for an AI assistant

There is an MCP server at https://cut.uniboards.co.uk/v2/mcp — streamable HTTP, stateless, nothing to install, and no key needed for the tools listed here. Point any MCP client at that URL:

{ "mcpServers": { "cut-optimiser": { "type": "http", "url": "https://cut.uniboards.co.uk/v2/mcp" } } }

Or ask it what it has, over plain HTTP:

curl -s -X POST 'https://cut.uniboards.co.uk/v2/mcp' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

https://cut.uniboards.co.uk/v2/mcp/info answers the same list as plain JSON, for a person or a crawler. The tools, asked of the server itself as this page is built:

  • optimise_cut_listOptimise a cut list. Work out how many boards a list of parts needs and where each part sits on each board.
  • read_cut_listRead a cut list out of a photo, scan, PDF or CSV. Transcribe a cut list from an image or document with a vision model: handwriting, a phone photo of a scribbled sheet, a supplier PDF, a spreadsheet export — behind a read permission of its own, which /mcp/info reports.
  • list_materialsFind a cuttable board in the Uniboards catalogue. Search the boards Uniboards actually cuts to size, by name, decor, thickness or size.
  • export_cut_planTurn a plan into a file for a workshop. Take the plan optimise_cut_list just returned and produce the file a workshop actually uses: a PDF to print, or a DXF for a CNC router.
  • price_cut_planPrice a plan that has already been worked out. Take the plan optimise_cut_list returned, plus the catalogue variant each material is, and get the price: per board, subtotal, VAT and total in GBP.
  • sheet_sizesSheet sizes and mm/feet conversion. Standard UK sheet material sizes, and conversion between millimetres and feet/inches.
  • make_machine_programTurn one machined part into a CNC part program. Build the file a CNC actually runs for ONE part: BPP for a Biesse, MPR for a Homag woodWOP.
  • make_sheet_programTurn a whole nested sheet into a CNC program. Build one program for a WHOLE sheet: cut every part out of the board and do the machining on each of them, in board coordinates.
  • read_machine_programRead a CNC program back into parts and machining. The other direction: give it a BPP, MPR, CIX or DXF and get the part back — size, thickness, its outline if it is not a rectangle, and every operation our editor can express.
  • shaker_doorLay out a Shaker front. A Shaker door described once — how wide the frame is, how many panels, where the rail sits — and answered as ready pockets, in the part's own coordinates.
  • shape_from_cutTurn one straight cut into the part's own outline. One straight cut across a rectangle (or across a shape you give), answered as BOTH halves with their areas.
  • list_machinesWhat machines we can write a file for. The machine catalogue: which machines get their own native program (BPP for a Biesse, MPR for a Homag woodWOP) and which get a DXF with their own layer names.

The server is stateless on purpose: a fresh server per request, no session id, nothing remembered between calls. Every tool above answers from its arguments alone, and none of them can reach a project, an order or a customer — the blast radius of a publicly reachable MCP server is whatever its tools can touch, and for a caller without a credential that is a packing engine and a table of sheet sizes. An administrator of this workshop's calculator, sending the admin session their own admin issues, additionally gets tools over that workshop's own projects; they are not part of this public API and are not listed here.

Also worth knowing
  • /llms.txt — the same facts written for a model that will not run JavaScript.
  • Board area divided by part area always UNDER-estimates the board count, because offcuts are the wrong shape. Only real packing gives a number you can order against.
  • A standard UK sheet is 2440 × 1220 mm. Egger MFC is 2800 × 2070 — a cut list drawn on an 8x4 assumption will be wrong for it.
  • Length runs along the grain, width across it. A material with can_rotate: false keeps every part the way you gave it.
  • This page is generated from the routers at boot, and the examples on it are the same objects the test suite posts. If something here is wrong, the code is wrong.