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": "nest-2026-09-28-trailing-corner-v6", "algorithm": "guillotine-remnants", "ms": 0.4 },
  "summary": {
    "boards_total": 1,
    "parts_placed": 4,
    "parts_requested": 4,
    "unplaced": 0,
    "average_occupancy_pct": 32.249,
    "calculation_proof": {
      "v": 1,
      "engine": { "algorithm": "guillotine-remnants", "version": "nest-2026-09-28-trailing-corner-v6" },
      "fingerprint": "<sha256-of-relative-layout>",
      "signature": "<server-hmac>"
    },
    "cuttable_on_saw": true,
    "offcuts": 2,
    "offcut_area_mm2": 1998719,
    "largest_offcut_mm2": 1501820,
    "scrap_area_mm2": 18081,
    "free_area_mm2": 2016800,
    "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": 2,
            "offcut_area_mm2": 1998719,
            "largest_offcut_mm2": 1501820,
            "scrap_area_mm2": 18081,
            "free_area_mm2": 2016800,
            "offcut_rects": [
              { "x": 604.5, "y": 0, "length": 2440, "width": 615.5 },
              { "x": 0, "y": 1618, "length": 822, "width": 604.5 }
            ],
            "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
              }
            ]
          }
        ]
      }
    ]
  },
  "warnings": []
}
  • engine — which build of the nesting engine produced this and how long it took. Informational.
  • summary.boards_total — how 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_mm2 — what 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.
  • sheets[].offcut_rects[] — the usable offcuts themselves, largest first: { x, y, length, width } in millimetres, in the same coordinates as the parts — x/y is the bottom-left corner, width runs along the board's width and length along its length. They are the pieces offcuts counts: both sides clear the threshold, the saw kerf and the edge trim are already off, and none overlaps a part or another offcut. An empty list means nothing on that board is worth keeping. To rack a piece and cut from it next time, send it back as materials[].stock: [{ length, width, qty: 1 }] (the engine works in whole millimetres, so 615.5 is cut as 615). /api/layout-pdf draws the largest pieces on every board with their sizes.
  • warnings — what is wrong with the plan even though it was made, in plain English: small parts asked to stay off the board edge that could not, narrow parts left on the edge in nesting-small-inside, a sheet a panel saw cannot cut, a list too big for one run. The plan is still returned; show these to the person rather than hiding them. Always an array of strings, empty when there is nothing to say.
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/configurator-boards The 3D tab’s boards (“MFC White 18 mm”) as catalogue boards, so parts sent from the 3D model can be priced and ordered. Name and sheet in; catalogue name, sheet, photo and variant out — no prices, and a board with no catalogue match stays your own.
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-keep Keep a copy of a file the page read by itself (a cut-list spreadsheet) for 90 days, so a wrong result can be checked against it. Answers only its SHA-256.
POST /api/import-keep/attach Tie files kept earlier (by SHA-256) to the project they were loaded into, once that project has been saved. Takes no file bytes.
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; the file is kept for 90 days.
POST /api/layout-dxf The same plan as DXF R12 for a CNC router, millimetres, one layer per purpose.
POST /api/layout-dxf-bundle The same plan as one DXF per sheet, all in one standard ZIP download — for a router without its own BPP/MPR format. Same body as layout-dxf; no project is stored.
POST /api/layout-pdf The cutting plan as a PDF: up to three layouts per page (two for near-square boards), 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-machining-presets Your saved machine tools, hinge systems and machining presets, available in every project on every signed-in device.
POST /api/my-machining-presets Create, edit or delete one of your machine tools, hinge systems or machining presets. The account and site are taken from the session.
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.
POST /api/parse-cut-list Read a cut list that is already text — CSV or TSV with a header row — into the materials /api/optimise takes. The same parser as the page’s CSV import; free, exact, no model.
POST /api/project-file/open-plan Open the Cabinet Lab kitchen of a project file as a new draft: the plan and its panels, the same way as Order cut list. Answers the draft's 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.
GET /api/shared-projects For a team leader: the saved cut lists of the team’s members, with owner and last cut-list change. Read-only, paged, optionally one member’s (member=email); no share tokens.
GET /api/shared-projects/:id One team member’s saved cut list, with board dimensions and parts for review. Team leader with a full workshop session only; anything else is 404.
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-bpp-bundle Several BPP sheet programs in one standard ZIP download. Each sheet remains a separate file inside; no project is stored.
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-mpr-bundle Several MPR sheet programs in one standard ZIP download. Same body as sheet-bpp-bundle.
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.
GET /api/team The signed-in customer’s team: for its leader, everyone in it with their number of saved lists and last change; for a member, who leads it.
POST /api/team/members Team leader adds a person (email, role member or leader). Only addresses on the team’s company domains approved by Uniboards; one team per person.
DELETE /api/team/members/:id Team leader removes a person from the team; their lists stop being visible at once. The last leader cannot be removed.
PATCH /api/team/members/:id Team leader changes a person’s role. A team always keeps at least one leader.
GET /api/test-piece The reference test piece - a 300 x 200 x 18 plate with an asymmetric hole flag, a recess, a through step and an outline. Send it to /api/machining-bpp with your machine settings and cut it from an offcut.
GET /api/test-piece-picture The picture of how the test piece must come out, face up - drawn from the same description.
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-keep Keep a copy of a file the page read by itself (a cut-list spreadsheet) for 90 days, so a wrong result can be checked against it. Answers only its SHA-256.
POST /api/v1/import-keep/attach Tie files kept earlier (by SHA-256) to the project they were loaded into, once that project has been saved. Takes no file bytes.
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; the file is kept for 90 days.
POST /api/v1/layout-dxf The same plan as DXF R12 for a CNC router, millimetres, one layer per purpose.
POST /api/v1/layout-dxf-bundle The same plan as one DXF per sheet, all in one standard ZIP download — for a router without its own BPP/MPR format. Same body as layout-dxf; no project is stored.
POST /api/v1/layout-pdf The cutting plan as a PDF: up to three layouts per page (two for near-square boards), 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/parse-cut-list Read a cut list that is already text — CSV or TSV with a header row — into the materials /api/optimise takes. The same parser as the page’s CSV import; free, exact, no model.
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-bpp-bundle Several BPP sheet programs in one standard ZIP download. Each sheet remains a separate file inside; no project is stored.
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-mpr-bundle Several MPR sheet programs in one standard ZIP download. Same body as sheet-bpp-bundle.
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 /api/v1/test-piece The reference test piece - a 300 x 200 x 18 plate with an asymmetric hole flag, a recess, a through step and an outline. Send it to /api/machining-bpp with your machine settings and cut it from an offcut.
GET /api/v1/test-piece-picture The picture of how the test piece must come out, face up - drawn from the same description.
GET /api/v1/workshop-machine The workshop machine every file from the shop site is written for: zero corner, CRN and Origins list (null on the tool site, where every machine is your own). Changed on the server only.
GET /api/workshop-machine The workshop machine every file from the shop site is written for: zero corner, CRN and Origins list (null on the tool site, where every machine is your own). Changed on the server only.
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_rotate — false 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_rotate — false 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 5.
  • 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. A piece from sheets[].offcut_rects of an earlier answer goes in as it is.

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": "nest-2026-09-28-trailing-corner-v6", "algorithm": "guillotine-remnants", "ms": 0.4 },
  "summary": {
    "boards_total": 1,
    "parts_placed": 4,
    "parts_requested": 4,
    "unplaced": 0,
    "average_occupancy_pct": 32.249,
    "calculation_proof": {
      "v": 1,
      "engine": { "algorithm": "guillotine-remnants", "version": "nest-2026-09-28-trailing-corner-v6" },
      "fingerprint": "<sha256-of-relative-layout>",
      "signature": "<server-hmac>"
    },
    "cuttable_on_saw": true,
    "offcuts": 2,
    "offcut_area_mm2": 1998719,
    "largest_offcut_mm2": 1501820,
    "scrap_area_mm2": 18081,
    "free_area_mm2": 2016800,
    "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": 2,
            "offcut_area_mm2": 1998719,
            "largest_offcut_mm2": 1501820,
            "scrap_area_mm2": 18081,
            "free_area_mm2": 2016800,
            "offcut_rects": [
              { "x": 604.5, "y": 0, "length": 2440, "width": 615.5 },
              { "x": 0, "y": 1618, "length": 822, "width": 604.5 }
            ],
            "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
              }
            ]
          }
        ]
      }
    ]
  },
  "warnings": []
}

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 5000. 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.
502—The engine answered with something we cannot use. Not your request.

Free and unmetered beyond the rate limit. Body up to 6mb; 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.
  • materials[].parts[] — the parts cut from it — {length, width, qty, edges: {l1, l2, w1, w2}}, tape thickness in mm per side (0.8, 1 or 2; 0 for none). Edging is priced from these, in whole rolls (roll_length_mm, rolls on each edging line), and an edged job takes the cut-and-edge service. Without parts there is no edging in the price.
  • extras[] — catalogue items bought with the cut — {kind: "catalogue", variant_id, qty} (hinges, handles, rails). Priced in the same total, a line each.

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.
502—The catalogue refused. Not your request.

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

POST /api/layout-pdf

The cutting plan as a PDF: up to three layouts per page (two for near-square boards), 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

  • labels — false 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": 2,
          "offcut_area_mm2": 1998719,
          "largest_offcut_mm2": 1501820,
          "scrap_area_mm2": 18081,
          "free_area_mm2": 2016800,
          "offcut_rects": [
            { "x": 604.5, "y": 0, "length": 2440, "width": 615.5 },
            { "x": 0, "y": 1618, "length": 822, "width": 604.5 }
          ],
          "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": 2,
          "offcut_area_mm2": 1998719,
          "largest_offcut_mm2": 1501820,
          "scrap_area_mm2": 18081,
          "free_area_mm2": 2016800,
          "offcut_rects": [
            { "x": 604.5, "y": 0, "length": 2440, "width": 615.5 },
            { "x": 0, "y": 1618, "length": 822, "width": 604.5 }
          ],
          "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, up to 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 6mb; 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": 2,
          "offcut_area_mm2": 1998719,
          "largest_offcut_mm2": 1501820,
          "scrap_area_mm2": 18081,
          "free_area_mm2": 2016800,
          "offcut_rects": [
            { "x": 604.5, "y": 0, "length": 2440, "width": 615.5 },
            { "x": 0, "y": 1618, "length": 822, "width": 604.5 }
          ],
          "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": 2,
          "offcut_area_mm2": 1998719,
          "largest_offcut_mm2": 1501820,
          "scrap_area_mm2": 18081,
          "free_area_mm2": 2016800,
          "offcut_rects": [
            { "x": 604.5, "y": 0, "length": 2440, "width": 615.5 },
            { "x": 0, "y": 1618, "length": 822, "width": 604.5 }
          ],
          "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 6mb; shares the layout-pdf request budget.

POST /api/layout-dxf-bundle

The same plan as one DXF per sheet, all in one standard ZIP download — for a router without its own BPP/MPR format. Same body as layout-dxf; no project is stored.

Required

  • materials[] — exactly the body /api/layout-dxf takes; every sheet becomes its own file. At most 200 sheets.

Optional

  • project_name — used in the ZIP name together with DXF and a UTC timestamp.
  • labels, layers — the same as /api/layout-dxf, applied to every sheet.

Request

One request, one ZIP download. Each file is byte for byte what /api/layout-dxf returns for a plan holding only that sheet; a refused sheet is named in X-Bundle-Failures and does not remove the others.

{
  "project_name": "Kitchen units",
  "materials": [
    {
      "ref": "mdf18",
      "title": "MDF 18",
      "panel": { "width": 1220, "length": 2440 },
      "sheets": [
        { "board": 1, "parts": [{ "name": "shelf (1)", "x": 0, "y": 0, "width": 600, "height": 400 }] },
        { "board": 2, "parts": [{ "name": "shelf (2)", "x": 0, "y": 0, "width": 600, "height": 400 }] }
      ]
    }
  ]
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/layout-dxf-bundle' \
  -o Kitchen_units_DXF_<UTC timestamp>.zip \
  -H 'Content-Type: application/json' \
  -d '{
  "project_name": "Kitchen units",
  "materials": [
    {
      "ref": "mdf18",
      "title": "MDF 18",
      "panel": { "width": 1220, "length": 2440 },
      "sheets": [
        { "board": 1, "parts": [{ "name": "shelf (1)", "x": 0, "y": 0, "width": 600, "height": 400 }] },
        { "board": 2, "parts": [{ "name": "shelf (2)", "x": 0, "y": 0, "width": 600, "height": 400 }] }
      ]
    }
  ]
}'

Answer

A file, not JSON:

Content-Type: application/zip
Content-Disposition: attachment; filename="Kitchen_units_DXF_<UTC timestamp>.zip"

A standard stored ZIP with MDF-18_sheet-1.dxf and MDF-18_sheet-2.dxf — the same names the page gives single sheet downloads. X-Bundle-Files says how many files are inside.

Refusals

StatuscodeWhat it means
422invalid_inputNo sheets, or more than 200.
422all_refusedNo sheet DXF could be built; failures lists why per 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 6mb; 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 — explicit panel thickness in mm; otherwise resolved from part.material or the top-level material, then machine settings.
  • material.actualThickness — measured thickness in mm, overriding the material nominal thickness/name. No nominal-to-actual correction is assumed.
  • part.machining[].through — for routing: false means depth from the face; true means depth is the overrun beyond the far face. Zero means exactly the far face. A missing flag retains legacy negative-depth support.
  • part.machining[].depthReference — "far-face" with through: true preserves an imported signed BPP DP: positive above the far face, negative below it.
  • 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; optional thickness is its nominal thickness.
  • 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".
  • material.actualThickness — measured thickness shared by part and sheet exports. Otherwise use nominal material thickness/name, saved fallback, then machine settings (18 mm by default).
  • sheet.panel.thickness — explicit thickness of this physical sheet/offcut; takes precedence over material thickness.
  • 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-bpp-bundle

Several BPP sheet programs in one standard ZIP download. Each sheet remains a separate file inside; no project is stored.

Required

  • jobs[] — 2–120 entries, each with material and sheet in the same shape as /api/sheet-bpp; at most 3000 placed parts.

Optional

  • project_name — used in the ZIP name together with format and UTC timestamp.
  • settings — the machine settings shared by all sheets.

Request

One request, one ZIP download. Every successful sheet keeps its own BPP; a refused sheet is named in X-Bundle-Failures and does not remove the others.

{
  "project_name": "Kitchen units",
  "jobs": [
    {
      "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] }]
          }
        ]
      }
    },
    {
      "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
      "sheet": {
        "board": 2,
        "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-bundle' \
  -o Kitchen_units_BPP_<UTC timestamp>.zip \
  -H 'Content-Type: application/json' \
  -d '{
  "project_name": "Kitchen units",
  "jobs": [
    {
      "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] }]
          }
        ]
      }
    },
    {
      "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
      "sheet": {
        "board": 2,
        "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/zip
Content-Disposition: attachment; filename="Kitchen_units_BPP_<UTC timestamp>.zip"

A standard stored ZIP with 1_MDF_18_sheet-1.bpp and 2_MDF_18_sheet-2.bpp. X-Bundle-Name gives the exact timestamped name for a folder chosen in the browser.

Refusals

StatuscodeWhat it means
422invalid_inputToo few/many sheets, or no sheet program could be built.
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 8mb; shares the layout-pdf request budget.

POST /api/sheet-mpr-bundle

Several MPR sheet programs in one standard ZIP download. Same body as sheet-bpp-bundle.

Required

  • jobs[] — the same material/sheet list as sheet-bpp-bundle.

Optional

  • project_name, settings — the same project name and machine settings.

Request

{
  "project_name": "Kitchen units",
  "jobs": [
    {
      "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] }]
          }
        ]
      }
    },
    {
      "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
      "sheet": {
        "board": 2,
        "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-bundle' \
  -o Kitchen_units_MPR_<UTC timestamp>.zip \
  -H 'Content-Type: application/json' \
  -d '{
  "project_name": "Kitchen units",
  "jobs": [
    {
      "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] }]
          }
        ]
      }
    },
    {
      "material": { "title": "MDF 18", "panel": { "length": 2440, "width": 1220, "thickness": 18 } },
      "sheet": {
        "board": 2,
        "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/zip
Content-Disposition: attachment; filename="Kitchen_units_MPR_<UTC timestamp>.zip"

A standard ZIP with one MPR per sheet, named and numbered like the BPP bundle.

Refusals

StatuscodeWhat it means
422invalid_inputThe same limits and per-sheet refusals as sheet-bpp-bundle.
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 8mb; 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 6mb; 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; the file is kept for 90 days.

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.
  • layers — DXF only: the role of a layer whose name says nothing we recognise, e.g. { "MY HOLES": "drill" }. Roles: outline, cutout, drill, pocket, groove, edge, label, material. A furniture DXF with an outline layer (“OUTSIDE CUT”, “OUTLINE”, “PROFILE”) comes back as one part per outline, with holes, pockets, cut-outs and edges read from layer names such as “10MM DIA DRILL 12MM DEPTH”; what is not understood is listed in notes, never guessed.
  • thickness — DXF only: the board thickness in mm, when no layer like “18MM EGGER” says it.
  • standard — DXF only: true also returns standardDxf — the same parts as a DXF in our layer standard (OUTSIDE_CUT_THROUGH, DRILL_10MM_DIA_12MM_DEPTH, POCKET_0.8MM_DEPTH, EDGE, LABEL, BOARD_18MM…), which this door reads back without questions. A DXF answer always carries layers (one line per layer: what it was read as and how we know), missing (what the drawing lacks — no layers, no depths, no thickness…) and clientMessage, a ready message to the customer.

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": [],
  "import_sha": "3f1c…e09a (SHA-256 of the file as kept)"
}

A file that turns out to be a whole SHEET — a sheet program, or the cut-plan DXF this service exports — 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. The file is kept for 90 days so a wrong result can be checked against it; import_sha is its SHA-256 (null if it could not be kept). A file this service made itself comes back with exported_by_us (when, project, sheet, machine corner) and a first note saying so: to change those parts, open the project it came from rather than reading the file back.

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 48mb; shares the layout-pdf request budget.

POST /api/import-keep

Keep a copy of a file the page read by itself (a cut-list spreadsheet) for 90 days, so a wrong result can be checked against it. Answers only its SHA-256.

Required

  • file — { name, base64 } — the file exactly as the person chose it.

Optional

  • result — { ok, rows } — what the page read from it.
  • project — the link token of the project it was loaded into; only a fingerprint of it is kept.
  • registry_id — the workshop project number it was loaded into.

Request

{
  "file": { "name": "kitchen.csv", "base64": "bmFtZSxsZW5ndGgsd2lkdGgscXR5CnNoZWxmLDYwMCw0MDAsNAo=" },
  "result": { "ok": true, "rows": 1 }
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/import-keep' \
  -H 'Content-Type: application/json' \
  -d '{
  "file": { "name": "kitchen.csv", "base64": "bmFtZSxsZW5ndGgsd2lkdGgscXR5CnNoZWxmLDYwMCw0MDAsNAo=" },
  "result": { "ok": true, "rows": 1 }
}'

Answer

{ "ok": true, "import_sha": "9b2f…41c0 (SHA-256 of the file as kept)" }

The file is kept for 90 days; import_sha is null if it could not be kept.

Refusals

StatuscodeWhat it means
422invalid_inputNo file.base64.
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 4mb; shares the optimise request budget.

POST /api/import-keep/attach

Tie files kept earlier (by SHA-256) to the project they were loaded into, once that project has been saved. Takes no file bytes.

Required

  • shas — SHA-256 of files kept earlier, up to 100.

Optional

  • project — the link token of the project they belong to.
  • registry_id — the workshop project number they belong to.

Request

{ "shas": ["<import_sha from import-keep or import-program>"], "registry_id": 47762 }
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/import-keep/attach' \
  -H 'Content-Type: application/json' \
  -d '{ "shas": ["<import_sha from import-keep or import-program>"], "registry_id": 47762 }'

Answer

{ "ok": true, "attached": 1 }

Only files already kept can be tied to a project; an unknown SHA is skipped, not created.

Refusals

StatuscodeWhat it means
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 32kb; shares the optimise request budget.

POST /api/parse-cut-list

Read a cut list that is already text — CSV or TSV with a header row — into the materials /api/optimise takes. The same parser as the page’s CSV import; free, exact, no model.

Required

  • text — the whole CSV or TSV as text, header row first. The header must name a Length and a Width column.

Optional

None.

Request

Columns: Description, Part ID, Material, Length, Width, Qty, Grain, L1, L2, W1, W2, "L1 mm"… for the exact tape, "Board name", "Board length mm", "Board width mm". Comma, semicolon or tab. Free, no engine run; the text is kept for 90 days. materials goes to /api/optimise as it is.

{
  "text": "Description,Part ID,Material,Length,Width,Qty,Grain,L1,L2,W1,W2,Hinges\nDoor,D1,18mm MDF,716,396,2,,Y,Y,Y,Y,2\nShelf,S1,18mm MDF,564,300,4,,Y,,,,\nSide,P1,MFC EGGER W1000 ST9 White,720,560,2,Y,2,,,,\nPlinth,X1,18mm MDF,,100,1,,,,,,\n"
}
curl -s -X POST 'https://cut.uniboards.co.uk/v2/api/parse-cut-list' \
  -H 'Content-Type: application/json' \
  -d '{
  "text": "Description,Part ID,Material,Length,Width,Qty,Grain,L1,L2,W1,W2,Hinges\nDoor,D1,18mm MDF,716,396,2,,Y,Y,Y,Y,2\nShelf,S1,18mm MDF,564,300,4,,Y,,,,\nSide,P1,MFC EGGER W1000 ST9 White,720,560,2,Y,2,,,,\nPlinth,X1,18mm MDF,,100,1,,,,,,\n"
}'

Answer

{
  "ok": true,
  "materials": [
    {
      "ref": "18mm MDF",
      "panel": { "length": 2440, "width": 1220 },
      "can_rotate": true,
      "parts": [
        {
          "length": 716,
          "width": 396,
          "qty": 2,
          "id": "D1",
          "name": "Door",
          "edges": { "l1": 1, "l2": 1, "w1": 1, "w2": 1 }
        },
        { "length": 564, "width": 300, "qty": 4, "id": "S1", "name": "Shelf", "edges": { "l1": 1 } }
      ]
    },
    {
      "ref": "MFC EGGER W1000 ST9 White",
      "panel": { "length": 2800, "width": 2070 },
      "can_rotate": false,
      "parts": [{ "length": 720, "width": 560, "qty": 2, "id": "P1", "name": "Side", "edges": { "l1": 2 } }]
    }
  ],
  "boards": [
    { "ref": "18mm MDF", "name": "18mm MDF" },
    { "ref": "MFC EGGER W1000 ST9 White", "name": "MFC EGGER W1000 ST9 White" }
  ],
  "rows_read": 3,
  "parts_total": 8,
  "skipped_lines": [5],
  "skipped_note": "Line(s) 5 have no usable length and width and were left out — nothing was guessed. Ask about them before you plan.",
  "columns": {
    "understood": [
      "Description",
      "Part ID",
      "Material",
      "Length",
      "Width",
      "Qty",
      "Grain",
      "L1",
      "L2",
      "W1",
      "W2",
      "Hinges"
    ],
    "ignored": []
  },
  "not_carried": [
    "line 2: hinges not carried — this part comes back as a plain rectangle. The web page places them from the same columns; a machine program for the part (make_machine_program, /api/machining-bpp) takes hinges as operations."
  ],
  "notes": [],
  "next": "Pass materials to optimise_cut_list (or /api/optimise) as they are. Board sizes come from the file's own columns, then its sheet-size word (8x4, 10x4, Egger), then the board name; check them if the customer named a different sheet."
}

Refusals

StatuscodeWhat it means
422not_a_cut_listNo header row naming both Length and Width.
422no_partsA header, but no row with a usable length and width; skipped_lines lists them.
422invalid_inputNo text, or longer than 3,000,000 characters.
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 6mb; shares the optimise 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 roughing passes overlap, per cent. Default 80.
  • passes — the cutters each panel is milled with, in order: [{tool, diameter, overlap, suffix}]. Default: 40mm rough (overlap 80), then 6MMCOMPR and 3MM round the perimeter only (overlap 0).

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-rough",
      "x": 470,
      "y": 250,
      "length": 780,
      "width": 340,
      "radius": 20,
      "depth": 6,
      "overlap": 80,
      "tool": "40mm",
      "diameter": 40
    },
    {
      "kind": "pocket",
      "id": "shaker-1-clean6",
      "x": 470,
      "y": 250,
      "length": 780,
      "width": 340,
      "radius": 8,
      "depth": 6,
      "overlap": 0,
      "tool": "6MMCOMPR",
      "diameter": 6
    },
    {
      "kind": "pocket",
      "id": "shaker-1-clean3",
      "x": 470,
      "y": 250,
      "length": 780,
      "width": 340,
      "radius": 8,
      "depth": 6,
      "overlap": 0,
      "tool": "3MM",
      "diameter": 3
    },
    {
      "kind": "pocket",
      "id": "shaker-2-rough",
      "x": 1430,
      "y": 250,
      "length": 980,
      "width": 340,
      "radius": 20,
      "depth": 6,
      "overlap": 80,
      "tool": "40mm",
      "diameter": 40
    },
    {
      "kind": "pocket",
      "id": "shaker-2-clean6",
      "x": 1430,
      "y": 250,
      "length": 980,
      "width": 340,
      "radius": 8,
      "depth": 6,
      "overlap": 0,
      "tool": "6MMCOMPR",
      "diameter": 6
    },
    {
      "kind": "pocket",
      "id": "shaker-2-clean3",
      "x": 1430,
      "y": 250,
      "length": 980,
      "width": 340,
      "radius": 8,
      "depth": 6,
      "overlap": 0,
      "tool": "3MM",
      "diameter": 3
    }
  ],
  "panels": 2,
  "note": "Put these on the part as machining, in this order. Every panel is three passes: 40mm rough (overlap 80), then 6MMCOMPR and 3MM round the perimeter only (overlap 0). 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": 10000
}

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.
  • cutCorrection — auto (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 a cut-out. Types are none (default), curve, ramp, or legacy auto. curve is BiesseWorks type 1 and, on a corrected contour, the woodWOP tangential approach (MDA="TAN"/MDE="TAN_AB"); none is type 0 and SEN/SEN_AB. ramp has no confirmed code in either program yet: it is written as none and returned as a warning. Legacy auto is curve on a through cut and none otherwise. An unknown type is written as none with a warning. These settings are for cutting parts out of the sheet only: grooves and pockets enter straight down, and a milled path takes its own leadIn/leadOut (or these, when it names none). A path's own auto means these on a through path (a notch or a cut-out) and none on a shallow one.
  • fileNameMax — the longest file name, without the extension, your machine loads (older controls stop at about 10). A longer name comes back in X-Program-Warnings; the file is still sent. Left out — no limit, nothing is checked.
  • 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 — production panel-saw optimisation: minimum boards first, then the lightest possible last board and its reusable remnant. Every layout has a straight-cut sequence.
  • nesting — compatibility alias for the current production optimiser. It presently returns the same panel-saw-safe plan as guillotine.
  • 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.
415unsupported_encodingThe body is compressed (Content-Encoding) or written in a character set we do not read. Send it uncompressed or with gzip/deflate, as UTF-8.
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
anon5000630
signed_in500018500
api_key500018500
premium5000362000
admin50007210000

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.

EndpointRequest budgetBody up to
POST /api/optimiseoptimise6mb
POST /api/quotequote6mb
POST /api/layout-pdflayout-pdf6mb
POST /api/layout-dxflayout-pdf6mb
POST /api/layout-dxf-bundlelayout-pdf6mb
POST /api/machining-bpplayout-pdf512kb
POST /api/machining-mprlayout-pdf512kb
POST /api/sheet-bpplayout-pdf1mb
POST /api/sheet-mprlayout-pdf1mb
POST /api/sheet-bpp-bundlelayout-pdf8mb
POST /api/sheet-mpr-bundlelayout-pdf8mb
POST /api/sheet-origins-csvlayout-pdf6mb
POST /api/import-programlayout-pdf48mb
POST /api/import-keepoptimise4mb
POST /api/import-keep/attachoptimise32kb
POST /api/parse-cut-listoptimise6mb
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_list — Optimise a cut list. Work out how many boards a list of parts needs and where each part sits on each board.
  • read_cut_list — Read 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.
  • parse_cut_list — Read a cut list you already have as text (CSV or TSV). Turn a cut list that is already text — a CSV or TSV with a header row naming Length and Width — into the materials optimise_cut_list takes, free and exact: the same parser the website uses for a CSV import, no model involved.
  • list_materials — Find a cuttable board in the Uniboards catalogue. Search the boards Uniboards actually cuts to size, by name, decor, thickness or size.
  • list_collections — List the ranges of the Uniboards catalogue. The board catalogue the way our page's board picker shows it: the ranges (MFC, MDF, plywood and so on), each with its handle and how many products it holds.
  • collection_boards — The boards in one range of the catalogue. Every board in one range from list_collections that we cut to size — the same list our page shows when that range is opened.
  • find_extras — Find hardware and other non-board items in the Uniboards catalogue. Search the rest of the catalogue — hinges, drawer runners, handles, glass, fixings: what a customer orders together with the cut boards.
  • export_cut_plan — Turn 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_plan — Price 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_sizes — Sheet sizes and mm/feet conversion. Standard UK sheet material sizes, and conversion between millimetres and feet/inches.
  • make_machine_program — Turn 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_program — Turn 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_program — Read 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_door — Lay 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_cut — Turn 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_machines — What 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.