Staircase3D
HomeAdvertiseSign in
NO
For website developers

Staircase data reception

Register only your business's HTTPS website address. When a customer chooses to share, the information is included as query parameters in the same GET request that opens your website. No API key or separate receiving endpoint is required. Your website may ignore the information or use it in a quote form. Opening the website without sharing adds no staircase data.

  1. Enter your quote page URL in the business card.
  2. The customer selects your business in the app and shares the staircase when opening your website.
  3. Your website reads the URL fields. Prefill a quote form or use the quantities in your own price calculator.

Website query fields

FieldContents
staircalc_versionWebsite query format version: 1.
staircalc_width_mm / staircalc_depth_mmOverall plan extents, not flight width.
staircalc_height_mmOverall height from the lowest to highest level.
staircalc_levelsNumber of staircase levels.
staircalc_detailsJSON array of staircase options for each level, shown below.
staircalc_quantitiesOptional JSON object containing the quantities used for the price estimate.
staircalc_data_urlOptional retrieval URL for the complete input. Expires after one hour.

All dimensions use millimetres with a decimal point. JSON fields are URL-encoded; URLSearchParams decodes them before JSON.parse. Existing website parameters and fragments are preserved. The completed URL is limited to 8,000 bytes, without silently truncating corners or levels. Websites imposing a lower URL limit may require adjustment.

Staircase options per level

[
  {
    "level": 1,
    "steps": 15,
    "rises": 16,
    "riser_mm": 175,
    "floor_height_mm": 2800,
    "tread_mm": 270,
    "tread_thickness_mm": 40,
    "flight_widths_mm": [
      1000,
      1000
    ],
    "nose": "rounded_top",
    "nose_mm": 25,
    "nose_overrides": [],
    "riser_board": "full",
    "stringer": "supported",
    "corners": [
      {
        "corner": 1,
        "type": "winder",
        "turn": "left",
        "angle_deg": 90
      }
    ],
    "inline_landing_flights": [],
    "merged_landing_flights": []
  }
]

steps counts physical treads, excluding landings. rises counts vertical rises, and riser_mm is the actual riser height. These two fields are omitted when an older archive cannot establish them. tread_mm is the configured going along the walking line. flight_widths_mm gives each flight's width. Levels, treads, flights and corners are numbered from 1, starting at the bottom.

nose: none, square, rounded_top, rounded_top_bottom, samba_square or samba_rounded. nose_mm is the shared nosing projection. nose_overrides lists individual tread overrides as objects with step, type and projection_mm. riser_board: none, full, child_safety or shaped_child_safety. stringer: closed_simple, closed_smooth, glued, supported, intermediate_support or boxed_winder.

Each corner has type landing or winder, turn left/right and its angle in degrees. inline_landing_flights identifies straight flights used as rest landings. merged_landing_flights identifies flights between corners that share a merged landing.

Quantities and price estimates

{
  "stringerMeters": 8.4,
  "steps": 15,
  "riserBoards": 15,
  "winderCorners": 1,
  "landingNodes": 0
}

On your account page, choose a currency and VAT-inclusive unit prices. The estimate multiplies stringer metres, treads, riser boards, winder corners and landing nodes by your rates and sums them. Stringer metres use the total stock length of physical stringer parts, excluding waste. Landings are counted per corner, including merged landings, plus once per straight rest landing. Bottom and top terminations are not landing nodes. A missing rate for a quantity actually present produces 'price on request'; 0 means explicitly included. The lowest complete estimate appears first within each currency. Delivery, installation and railings are excluded.

Using the website fields

This example prefills a form with the first staircase level. Place the script after the form, or load it with defer. Adapt the field names to your website. For multiple levels, use one set of fields per item in details.

<form id="staircase-quote">
  <label>Steps <input name="steps" type="number"></label>
  <label>Riser height (mm) <input name="riser_mm" type="number" step="any"></label>
  <label>Nosing <input name="nose"></label>
  <label>Riser board <input name="riser_board"></label>
  <label>Corners <textarea name="corners"></textarea></label>
</form>
<p id="staircase-data-status" role="status"></p>
const query = new URLSearchParams(window.location.search);
function readJSON(name, fallback) {
  try { return JSON.parse(query.get(name) || "null") ?? fallback; }
  catch { return fallback; }
}
const supported = query.get("staircalc_version") === "1";
const rawDetails = supported ? readJSON("staircalc_details", []) : [];
const details = Array.isArray(rawDetails) ? rawDetails : [];
const quantities = supported ? readJSON("staircalc_quantities", null) : null;
const firstLevel = details[0];
const form = document.querySelector("#staircase-quote");
function fill(name, value) {
  const field = form?.elements.namedItem(name);
  if (field && "value" in field && !field.value && value != null) {
    field.value = String(value); // Do not insert URL data as HTML.
  }
}
if (firstLevel && typeof firstLevel === "object") {
  if (Number.isInteger(firstLevel.steps) && firstLevel.steps >= 0) {
    fill("steps", firstLevel.steps);
  }
  if (Number.isFinite(firstLevel.riser_mm) && firstLevel.riser_mm > 0) {
    fill("riser_mm", firstLevel.riser_mm);
  }
  if (typeof firstLevel.nose === "string") fill("nose", firstLevel.nose);
  if (typeof firstLevel.riser_board === "string") fill("riser_board", firstLevel.riser_board);
  if (Array.isArray(firstLevel.corners)) {
    fill("corners", JSON.stringify(firstLevel.corners));
  }
}

Ordinary visits without staircase data leave the form empty. Customers can check and edit the prefilled fields before submitting. Validate every value on your server before calculating a binding quote. Your developer can connect the same fields to an existing form in your website platform.

Optional: retrieve the complete input

The temporary URL is an access credential: anyone holding it can retrieve the data for one hour. The complete input is stored encrypted, and expired rows are cleaned up every minute. Retrieval also stops if the website address changes or the listing is no longer active in the selected area. Browser code can fetch it with CORS without signing in. No supplier API key is needed; do not log or forward the URL. Dimensions and staircase options can still accompany the website link when the complete input is unavailable.

Continue in the same script below if you need more than the URL fields. Use project.levels for your own calculation or let the customer submit the input with your quote form to your server. Retrieve it while the link is valid; it is not a permanent archive URL.

async function receiveCompleteStaircase() {
  const source = supported ? query.get("staircalc_data_url") : null;
  if (!source) return null;
  try {
    const url = new URL(source);
    if (url.origin !== "https://aedbbkplnmmimkacxrup.supabase.co" ||
        url.pathname !== "/functions/v1/get-project-share" ||
        url.username || url.password ||
        !/^[0-9a-f]{64}$/.test(url.searchParams.get("token") || "")) {
      throw new Error("Invalid Staircase3D data URL");
    }
    const response = await fetch(url, {
      credentials: "omit", cache: "no-store", redirect: "error",
      referrerPolicy: "no-referrer", signal: AbortSignal.timeout(10000)
    });
    if (!response.ok) throw new Error("Staircase data unavailable");
    const project = await response.json();
    if (project.schemaVersion !== 1 || !Array.isArray(project.levels)) {
      throw new Error("Unsupported staircase data");
    }
    return project;
  } catch {
    const status = document.querySelector("#staircase-data-status");
    if (status) status.textContent =
      "Complete data is unavailable. You can still use the prefilled form.";
    return null;
  }
}
// For example, inside your async form-initialization function:
// const project = await receiveCompleteStaircase();
// project?.levels[n].input: complete editable staircase input
// project?.levels[n].summary: summary for this level
// project?.pricingBasis: quantities for your own price calculation
receiveCompleteStaircase().then((project) => {
  if (!project) return;
  const status = document.querySelector("#staircase-data-status");
  if (status) status.textContent =
    "Received " + project.levels.length + " staircase level(s).";
  // Connect project to your own quote form or calculator here.
});

The complete JSON object has schemaVersion, deliveryID, sentAt, units, overallDimensions, levels and optional pricingBasis. Each level has id, levelNumber, elevationMillimeters, dimensions, input and summary. input contains the complete editable staircase input. Project names, photos, notes, part archives and 3D meshes are excluded. These are customer-supplied planning details, not a confirmed order.

© 2026 Widding Systems AS
Website staircase dataPrivacyTermsWidding Systems