# FERS Cloud — full documentation > FERS is a finite-element solver for 3D beams, frames and trusses with > Eurocode EN 1993-1-1 steel checks. The same Rust engine runs in the > browser (WebAssembly), in Python, in Node and behind a REST API. > Free tier: models up to 100 members, no account required. Hub: https://ferscloud.com/getting-started Index: https://ferscloud.com/llms.txt Machine-readable API: https://ferscloud.com/api/openapi.json MCP endpoint: https://ferscloud.com/api/mcp Each section below is also available on its own at `https://ferscloud.com/docs/.md`. --- # Quickstart: your first model in the browser > Everything here happens in the web app — no install, no coding, and no account needed to solve. Open a worked example to see the whole flow in one click, then build your own model by working through the Data Workspace tabs in order. Source: https://ferscloud.com/docs/quickstart Last updated: 2026-09-04 ## Fastest route: open an example The quickest way to get a feel for FERS is to solve something that already works. From the [home page](https://ferscloud.com/), pick **Simple Shed** or **Solar Rail** and press **Calculate**. You get displacements, axial/shear/moment diagrams and reactions on a model that is known to be correct, so you can learn the interface without also debugging your own geometry. The [examples library](https://ferscloud.com/examples) has more, including a [portal frame](https://ferscloud.com/portal-frame-analysis-example) and a full [EN 1993-1-1 steel member check](https://ferscloud.com/eurocode-3-steel-beam-check-example) with a per-clause hand-calculation trace. > Prefer to start from a form rather than a 3D model? The [free 2D frame calculator](https://ferscloud.com/free-2d-frame-calculator) and the [beam calculators](https://ferscloud.com/calculators) solve with the same engine and need nothing set up at all. ## Build your own model Work through the Data Workspace tabs in this order. Each step depends on the one before it, which is why the tabs are arranged this way. 1. **Materials** — E-modulus, density and yield stress — for example S235 steel. Everything downstream references a material, so this comes first. 2. **Sections** — Cross-section properties (area, `i_y`, `i_z`, `j`), each referencing a material. Bending about the strong axis uses `i_z` — see [conventions and units](https://ferscloud.com/conventions) before you type a second moment of area. 3. **Nodes** — The X, Y, Z coordinates of your structure's joints. Y is up. 4. **Nodal supports** — Boundary conditions. A pinned support fixes Ux/Uy/Uz and leaves rotations free; assign the support to a node from the Nodes tab once you have defined it. 5. **Members** — Connect two nodes with a section. A member is a 2-node beam element with six degrees of freedom per node. 6. **Load cases and loads** — Create a load case (for example "Permanent"), then add nodal, distributed or surface loads to it. Directions are global X/Y/Z, so `(0, -1, 0)` is straight down. 7. **Calculate** — Run the analysis from the top bar. Results appear in the Results tabs and in the viewport. > The welcome tour walks through the same interface interactively — replay it any time from **Help (?) → Show welcome tour**. ## Read the results After a solve, the Results tabs carry nodal displacements, member internal forces, reactions and — if you added them — unity checks. The viewport draws the deflected shape and the diagram you select. Diagrams are exact rather than interpolated: member displacements are sampled from the solved element, so a fixed-fixed beam under a uniformly distributed load reports the true wL⁴/384EI sag rather than a straight-line approximation between end values. Before trusting a large model, solve a single-span case whose closed-form answer you know and compare. The [accuracy benchmarks](https://ferscloud.com/benchmarks) and the [NAFEMS benchmark set](https://ferscloud.com/nafems) do exactly this, in the browser, with the target-versus-FERS error shown for each. ## Save and export Your model can be exported as JSON from the **Download** menu at any point — that file is the same format the Python package and the REST API consume, so a model started in the browser can be finished in a script. Saved cloud storage is part of the Pro plan; on the free tier you keep models as local JSON. The free tier solves models up to 100 members, Pro up to 10,000. ## Where to go next - [Install the Python package](https://ferscloud.com/docs/installation) to drive the same solver from a script. - [Use it from JavaScript](https://ferscloud.com/docs/javascript) — the solver is published as a WebAssembly npm package that runs in the browser with no server round-trip. - [Call the REST API](https://ferscloud.com/docs/rest-api) from any language, or import the OpenAPI spec into a client generator. - [Connect an AI agent](https://ferscloud.com/mcp) — Claude, ChatGPT, Cursor and VS Code can drive FERS over MCP. - [Keyboard shortcuts](https://ferscloud.com/docs/keyboard-shortcuts) for the modelling tools once you are past the first model. ## Frequently asked questions **Do I need an account to solve a model?** No. Solving runs in your browser and needs no account and no API key. An account is only needed for cloud model storage, API keys and the agent channel. **How big a model can I solve for free?** Up to 100 members. Beyond that the solver returns a LimitExceeded error; Pro raises the ceiling to 10,000 members. **Does my model leave my machine?** Not when you solve in the browser. The solver is a WebAssembly build of the same Rust engine, so the model and the results stay on your device unless you explicitly save to the cloud or share it. **Which units does FERS use?** SI base units throughout the model JSON: metres, newtons and pascals. Angles in the JSON contract are degrees. The full set of axis and sign conventions is on the [conventions page](https://ferscloud.com/conventions). **Why is my beam bending about the wrong axis?** Almost always because `i_y` and `i_z` are swapped. In FERS the strong bending axis is the local z axis, so a beam bending in its usual plane uses `i_z`. See [conventions and units](https://ferscloud.com/conventions). **Can I import a model I built somewhere else?** You can import the FERS model JSON format directly. There is no native import for other vendors' file formats; the practical route is to generate the JSON from a script — see [the Python API](https://ferscloud.com/docs/python-api). ## Related - https://ferscloud.com/docs/installation - https://ferscloud.com/docs/keyboard-shortcuts - https://ferscloud.com/docs/javascript --- # Install the FERS Python package > One pip command gets you the Python modelling layer and the compiled Rust solver. Local analysis needs no account, no key and no network — credentials are only for cloud storage, the agent channel and Pro limits. Source: https://ferscloud.com/docs/installation Last updated: 2026-09-04 ## 1. Install the package The package is published on PyPI as [FERS](https://pypi.org/project/fers/). It ships the Python model-building layer plus the compiled solver wheel, so there is nothing else to build. ```bash pip install FERS ``` > Requires Python 3.11 or newer. Wheels are published for Windows, macOS and Linux — no Rust toolchain is needed to install. ## 2. Verify the installation Import the core objects and construct an empty model. If this runs without raising, the compiled solver loaded correctly. ```python from fers_core import FERS, Node, Member, Section, Material, NodalSupport, NodalLoad # Quick smoke-test model = FERS() print("FERS installed successfully") ``` ## 3. Authenticate with the cloud (optional) You only need this for saving models to your account, the MCP agent channel, and Pro limits. There are two ways to authenticate, depending on how you signed up. 1. **Option A — email and password account** — Exchange your credentials for a 1-hour Bearer token via `POST /api/sdk/token`. Only works for accounts created with an email and password. 2. **Option B — Google or GitHub account (recommended for everyone)** — Create a permanent API key from your [Profile page](https://ferscloud.com/profile) → API Keys. No password required, it works for any sign-in method, and you can revoke it at any time. ## Option A: short-lived Bearer token The token lasts one hour. Fetch a fresh one when it expires rather than storing it. ```python import requests resp = requests.post("https://ferscloud.com/api/sdk/token", json={ "email": "you@example.com", "password": "your_password", }) token = resp.json()["token"] ``` ## Option B: persistent API key An API key has the shape `.` and is shown once, at creation. Store it in an environment variable and keep it server-side — never in browser code or a committed file. ```python import os os.environ["FERS_API_KEY"] = "." ``` > Cloud authentication is only needed for saving models, MCP and Pro features. Local analysis with `run_analysis()` works without any credentials. ## What you get without paying The free tier solves models up to 100 members, with no account and no key. Pro raises that to 10,000 members and adds cloud model storage. The agent and REST channels are metered separately: 100 free solves per rolling week, then €0.01 per solve from prepaid credit. Pro is €19.95/month. | | Free | Pro | | --- | --- | --- | | Members per model | 100 | 10,000 | | Local solving | Unlimited | Unlimited | | Account required | No | Yes | | Cloud model storage | No | Yes | | Solves per week via API/MCP | 100 | Unlimited | ## Next steps - [Build and solve a model](https://ferscloud.com/docs/python-api) — the core objects and a first analysis. - [Worked examples](https://ferscloud.com/docs/python-examples) — cantilever, simply supported beam, portal frame and an EN 1993-1-1 check, each with its closed-form hand check. - [Why script a calculation at all](https://ferscloud.com/structural-analysis-python) — the case for the Python route. ## Frequently asked questions **What is the pip package called?** `pip install FERS`. The import name is `fers_core` — for example `from fers_core import FERS, Node, Member`. **Do I need an API key to run an analysis?** No. `run_analysis()` solves locally with no credentials and no network access. Keys are only for cloud model storage, the MCP/REST channels and Pro limits. **Which Python versions are supported?** Python 3.11 and newer, on Windows, macOS and Linux. The solver ships as a prebuilt wheel, so installing does not require a Rust toolchain. **Where do I create an API key?** On your [Profile page](https://ferscloud.com/profile) under API Keys. The key is displayed once at creation — store it then, because it cannot be shown again. You can revoke it at any time. **Can I use the solver from JavaScript instead?** Yes. The same engine is published as a WebAssembly npm package that runs in the browser or in Node with no server round-trip — see [using FERS from JavaScript](https://ferscloud.com/docs/javascript). **Is my model uploaded anywhere when I solve locally?** No. Local solving runs entirely in your process. Nothing is sent to FERS Cloud unless you explicitly save a model or call one of the REST endpoints. ## Related - https://ferscloud.com/docs/quickstart - https://ferscloud.com/docs/python-api --- # Build and solve a model in Python > Seven objects cover almost every model: Node, Material, Section, Member, MemberSet, NodalSupport and NodalLoad. You assemble them, call `run_analysis()`, and read a results bundle keyed by load case. Source: https://ferscloud.com/docs/python-api Last updated: 2026-09-04 ## The core objects Everything is imported from `fers_core`. A model is a `FERS` instance holding member sets; loads hang off load cases rather than off the model directly, so the same geometry can carry several independent load cases. - **Node** — an X, Y, Z point. Y is up. Nodes own their support condition. - **Material** — E-modulus, shear modulus, density, yield stress. All SI: pascals and kg/m³. - **Section** — area and second moments of area, referencing a material. - **Member** — a 2-node beam element with six degrees of freedom per node, referencing a section. - **MemberSet** — the group you add to the model; also the unit design checks are applied to. - **NodalSupport** — the boundary condition assigned to a node. - **NodalLoad** — a force or moment applied to a node within a load case. ```python from fers_core import ( FERS, Node, Member, Section, Material, MemberSet, NodalSupport, NodalLoad ) ``` ## Define geometry, material and section Coordinates are in metres and material properties in pascals. The section below is an IPE 180 taken from the FERS steel section library. Note the axis convention, because it is the single most common source of a wrong answer: `i_z` is the **strong** axis and `i_y` the weak one. A beam bending in its usual plane is governed by `i_z`. ```python # Nodes (x, y, z coordinates in metres) node1 = Node(0, 0, 0) node2 = Node(5, 0, 0) # Material (Steel S235) steel = Material( name="Steel S235", e_mod=210e9, # Pa g_mod=80.769e9, # Pa density=7850, # kg/m³ yield_stress=235e6 # Pa ) # Cross-section (IPE 180), values from the FERS steel section library. # Axis convention: i_z is the STRONG axis (bending in the local x-y plane), # i_y the weak axis — see ferscloud.com/conventions section = Section( name="IPE 180", material=steel, i_y=1.009e-6, # m⁴ (weak axis) i_z=13.17e-6, # m⁴ (strong axis) j=0.0477e-6, # m⁴ area=0.00240 # m² ) ``` > Full axis, sign and unit conventions are on the [conventions page](https://ferscloud.com/conventions). The JSON contract takes angles in degrees, not radians. ## Create the member, apply a support and a load A bare `NodalSupport()` is fully fixed. For a pin or a roller, pass explicit `displacement_conditions` and `rotation_conditions` — see [the worked examples](https://ferscloud.com/docs/python-examples) for both. Load directions are global unit vectors, so `(0, 1, 0)` with a negative magnitude is a downward force. ```python # Create beam element beam = Member(start_node=node1, end_node=node2, section=section) # Fixed support at node1 node1.nodal_support = NodalSupport() # Organise members into a set and add to model model = FERS() model.add_member_set(MemberSet(members=[beam])) # Load case with a −1 kN point load at node2 lc = model.create_load_case(name="Gravity") NodalLoad(node=node2, load_case=lc, magnitude=-1000, direction=(0, 1, 0)) ``` ## Run the analysis and read results `run_analysis()` solves every load case. Results come back on `model.resultsbundle`, keyed by load-case name, with displacements and reactions keyed by node id as a string. The comments show the actual output for these exact inputs. ```python model.run_analysis() results = model.resultsbundle.loadcases["Gravity"] dy = results.displacement_nodes["2"].dy Vy = results.reaction_nodes["1"].nodal_forces.fy Mz = results.reaction_nodes["1"].nodal_forces.mz print(f"Tip deflection : {dy*1000:.3f} mm") # -15.065 mm print(f"Reaction Vy : {Vy:.1f} N") # 1000.0 N print(f"Reaction Mz : {Mz:.1f} Nm") # 5000.0 Nm ``` > Hand check: a 5 m cantilever bending about its strong axis gives δ = PL³ / 3EI_z = 1000·5³ / (3 · 210×10⁹ · 13.17×10⁻⁶) = 15.07 mm, with a 1 kN reaction and a 5 kN·m fixing moment from equilibrium. The solver returns 15.065 mm. ## Beyond linear statics The same model object carries the settings for the other analysis types. Second-order (P-Delta) analysis, linear buckling with a critical load factor, modal analysis and response-spectrum seismic analysis are all driven from analysis settings rather than from a different API. Design checks are attached to member sets and evaluated after the solve, returning per-clause utilizations with the intermediate values that produced them — so a check can be read as a hand calculation rather than a single number. - Verify against closed-form solutions first — see [accuracy benchmarks](https://ferscloud.com/benchmarks) and the [NAFEMS set](https://ferscloud.com/nafems). - For EN 1993-1-1 member checks, `check_beam` builds, solves and checks a single span in one call — see [the examples](https://ferscloud.com/docs/python-examples). ## Frequently asked questions **Why are my displacements keyed by a string?** Result dictionaries are keyed by node and member id as strings, because the results bundle round-trips through JSON. `displacement_nodes["2"]`, not `[2]`. **What is a MemberSet for?** It is the unit you add to the model and the unit design checks apply to. Grouping members that form one physical element — a column spliced from two members, say — lets a buckling length or a code check span the whole thing. **How do I model a pin or a roller?** Pass explicit conditions to `NodalSupport`. A pin fixes the three translations and leaves bending rotations free; hold the torsional rotation as well or the member has a rigid-body twist mode and the solve is singular. The [simply supported example](https://ferscloud.com/docs/python-examples) shows both. **Which second moment of area is the strong axis?** `i_z`. FERS treats local z as the strong bending axis throughout — the solver, the design checks and the diagrams all agree on this. Swapping `i_y` and `i_z` is the most common modelling error. **Do I need an internet connection to solve?** No. `run_analysis()` runs the compiled solver in your own process. Network access is only used if you call a cloud endpoint. **Can I get the deflected shape rather than just end values?** Yes, and it is load-exact rather than interpolated between end displacements — a fixed-fixed beam under a uniformly distributed load reports the true wL⁴/384EI sag. Request it in the analysis options. ## Related - https://ferscloud.com/docs/installation - https://ferscloud.com/docs/python-examples --- # Worked examples > Four scripts you can paste and run. Each one states the expected output for its exact inputs and the closed-form solution it was checked against, so you can confirm your install reproduces them before trusting a model of your own. Source: https://ferscloud.com/docs/python-examples Last updated: 2026-09-04 ## 1. Cantilever with an end load A 5 m IPE 180 cantilever, fully fixed at one end, carrying a 1 kN downward point load at the tip. This is the simplest model that exercises the whole pipeline: geometry, material, section, support, load case and solve. ```python from fers_core import FERS, Node, Member, Section, Material, MemberSet, NodalSupport, NodalLoad model = FERS() node1 = Node(0, 0, 0) # fixed end node2 = Node(5, 0, 0) # free end (5 m span) steel = Material(name="Steel S235", e_mod=210e9, g_mod=80.769e9, density=7850, yield_stress=235e6) # IPE 180 from the FERS section library: i_z = strong axis (in play here), i_y = weak axis section = Section(name="IPE 180", material=steel, i_y=1.009e-6, i_z=13.17e-6, j=0.0477e-6, area=0.00240) beam = Member(start_node=node1, end_node=node2, section=section) node1.nodal_support = NodalSupport() # fully fixed model.add_member_set(MemberSet(members=[beam])) lc = model.create_load_case(name="End Load") NodalLoad(node=node2, load_case=lc, magnitude=-1000, direction=(0, 1, 0)) model.run_analysis() dy = model.resultsbundle.loadcases["End Load"].displacement_nodes["2"].dy print(f"Tip deflection: {dy*1000:.3f} mm") # Expected output: Tip deflection: -15.065 mm # Hand check: δ = PL³/3EI_z = 1000·5³ / (3 · 210e9 · 13.17e-6) = 15.07 mm ✓ ``` > Hand check: δ = PL³ / 3EI_z = 1000·5³ / (3 · 210×10⁹ · 13.17×10⁻⁶) = 15.07 mm. The solver returns 15.065 mm. ## 2. Simply supported beam with a centre load A 10 m IPE 300 in two 5 m members so there is a node at mid-span to load and to read. A pin at one end, a roller at the other. Note the explicit support conditions. The pin holds the three translations and the torsional rotation but leaves the bending rotations free — hold the torsion or the beam has a rigid-body twist mode and the stiffness matrix is singular. The roller additionally releases longitudinal translation so the beam can extend freely. ```python from fers_core import FERS, Node, Member, Section, Material, MemberSet, NodalSupport, NodalLoad model = FERS() node1 = Node(0, 0, 0) node2 = Node(5, 0, 0) # mid-span node3 = Node(10, 0, 0) # right support steel = Material(name="Steel S235", e_mod=210e9, g_mod=80.769e9, density=7850, yield_stress=235e6) section = Section(name="IPE 300", material=steel, i_y=6.038e-6, i_z=83.58e-6, j=0.199e-6, area=0.00538) beam1 = Member(start_node=node1, end_node=node2, section=section) beam2 = Member(start_node=node2, end_node=node3, section=section) # Pin at node1 — bending rotations free, torsion (RX) held so the beam # has no rigid-body twist mode. Roller at node3 — X translation free too. pin = NodalSupport( displacement_conditions={"X": "Fixed", "Y": "Fixed", "Z": "Fixed"}, rotation_conditions={"X": "Fixed", "Y": "Free", "Z": "Free"}, ) roller = NodalSupport( displacement_conditions={"X": "Free", "Y": "Fixed", "Z": "Fixed"}, rotation_conditions={"X": "Free", "Y": "Free", "Z": "Free"}, ) node1.nodal_support = pin node3.nodal_support = roller model.add_member_set(MemberSet(members=[beam1, beam2])) lc = model.create_load_case(name="Centre Load") NodalLoad(node=node2, load_case=lc, magnitude=-10000, direction=(0, 1, 0)) model.run_analysis() dy_mid = model.resultsbundle.loadcases["Centre Load"].displacement_nodes["2"].dy print(f"Mid-span deflection: {dy_mid*1000:.3f} mm") # Expected output: Mid-span deflection: -11.870 mm # Hand check: δ = PL³/48EI_z = 10000·10³ / (48 · 210e9 · 83.58e-6) = 11.87 mm ✓ ``` > Hand check: δ = PL³ / 48EI_z = 10000·10³ / (48 · 210×10⁹ · 83.58×10⁻⁶) = 11.87 mm. The solver returns 11.870 mm. ## 3. Portal frame under horizontal load Two 4 m HEA 200 columns with fixed bases and a 6 m IPE 300 rafter, loaded by a 5 kN horizontal force at the top of the left column. This is the smallest model where frame action — rather than a single member — determines the answer. ```python from fers_core import FERS, Node, Member, Section, Material, MemberSet, NodalSupport, NodalLoad model = FERS() # Portal frame: two columns (h=4 m) + one beam (L=6 m) n1 = Node(0, 0, 0) # left base n2 = Node(0, 4, 0) # left top n3 = Node(6, 4, 0) # right top n4 = Node(6, 0, 0) # right base steel = Material(name="Steel S235", e_mod=210e9, g_mod=80.769e9, density=7850, yield_stress=235e6) col_sec = Section(name="HEA 200", material=steel, i_y=13.36e-6, i_z=36.93e-6, j=0.206e-6, area=0.00538) beam_sec = Section(name="IPE 300", material=steel, i_y=6.038e-6, i_z=83.58e-6, j=0.199e-6, area=0.00538) col_left = Member(start_node=n1, end_node=n2, section=col_sec) col_right = Member(start_node=n4, end_node=n3, section=col_sec) rafter = Member(start_node=n2, end_node=n3, section=beam_sec) n1.nodal_support = NodalSupport() # fixed base n4.nodal_support = NodalSupport() # fixed base model.add_member_set(MemberSet(members=[col_left, col_right, rafter])) lc = model.create_load_case(name="Wind") # Horizontal wind load on left column top NodalLoad(node=n2, load_case=lc, magnitude=5000, direction=(1, 0, 0)) model.run_analysis() dx = model.resultsbundle.loadcases["Wind"].displacement_nodes["2"].dx print(f"Sway at left top: {dx*1000:.3f} mm") # Expected output: Sway at left top: 5.310 mm # Hand check (slope-deflection, axial deformation neglected): 5.30 mm ✓ ``` > Hand check by slope-deflection, neglecting axial deformation: 5.30 mm. The solver returns 5.310 mm. A fuller treatment with diagrams is on the [portal frame worked example](https://ferscloud.com/portal-frame-analysis-example). ## 4. Eurocode 3 steel member check `check_beam` builds a single-span beam, solves it and runs the EN 1993-1-1 member checks in one call. Here: a 7.5 m simply supported IPE 400 in S355 under a 13 kN/m characteristic UDL, factored by 1.35, with the compression flange unrestrained over the full span. The result carries a per-clause trace rather than a single number, so each utilization can be read as a hand calculation — the intermediate values that produced it are all there. ```python from fers_core import check_beam # 7.5 m simply supported IPE 400 in S355, 13 kN/m characteristic UDL, # ULS factor 1.35, compression flange unrestrained over the full span model = check_beam( span=7.5, section="IPE400", material="S355", udl=13_000, # N/m characteristic (13 kN/m) uls_factor=1.35, ) model.run_analysis() check = model.unity_check_results()[0] print(f"Governing UC: {check['max_utilization']:.2f}") for step in check["governing"]["trace"]: print(f" {step['label']:22s} {step['value']:.2f}") # Expected output (solver-computed, matches the published worked example): # Governing UC: 0.82 # Bending y (6.2.5) 0.00 # Bending z (6.2.5) 0.27 # Shear z (6.2.6) 0.00 # Shear y (6.2.6) 0.07 # Combined N+M (6.2.1) 0.27 # LTB (6.3.2) 0.82 # Governing 0.82 ``` > This reproduces the published [Eurocode 3 worked example](https://ferscloud.com/eurocode-3-steel-beam-check-example), where the same case is set out clause by clause. Lateral-torsional buckling governs at UC 0.82. ## Verify before you trust Every example above prints a number next to the closed-form solution it should reproduce. That is deliberate, and it is the habit worth copying: solve a case whose answer you already know before scaling up to one you do not. The [accuracy benchmarks](https://ferscloud.com/benchmarks) do this across a wider set of closed-form cases, and the [NAFEMS page](https://ferscloud.com/nafems) runs the standard finite-element benchmarks live in your browser with the target-versus-FERS error for each. ## Frequently asked questions **Why does the simply supported example need two members?** Only so there is a node at mid-span to apply the load to and read the displacement from. The element formulation is exact for a prismatic member, so splitting the span does not change the answer — it just gives you somewhere to attach the load. **Why does the pin hold the torsional rotation?** Releasing all three rotations at both ends of a straight beam leaves it free to spin about its own axis. That rigid-body mode makes the stiffness matrix singular and the solve fails. Holding RX at one support removes the mode without restraining any bending. **My numbers differ in the last digit — is something wrong?** No. The printed values are rounded to three decimals of a millimetre; the closed-form comparison is quoted to the precision the formula justifies. A difference beyond that usually means swapped `i_y` / `i_z` or a units mistake. **Where do the section properties come from?** The FERS steel section library, which ships with the package. You can also pass properties directly, as these examples do, or compute them for an arbitrary shape with the [section properties calculator](https://ferscloud.com/section-properties-calculator). **Can I run these without an account?** Yes. All four solve locally with no credentials. Only cloud storage and the metered API/agent channels need a key. **How do I do the same thing from JavaScript?** The same engine is published as a WebAssembly npm package with an identical model format — see [using FERS from JavaScript](https://ferscloud.com/docs/javascript). ## Related - https://ferscloud.com/docs/python-api - https://ferscloud.com/docs/rest-api --- # Use FERS from JavaScript > The FERS solver is published as a WebAssembly build of the same Rust engine that powers the web app. It runs in the browser or in Node, solves without a server round-trip, and needs no API key on the free tier. Source: https://ferscloud.com/docs/javascript Last updated: 2026-09-04 ## Install Two packages are published from the same engine and share an identical API — only the installation and bundler setup differ. Pick `-web` for anything that goes through a bundler, and the unsuffixed package for server-side Node. ```bash # Browser apps (Vite, Next.js, webpack): npm install @ferscloud/fers-calculation-web # Node.js / server-side: npm install @ferscloud/fers-calculation ``` > Both packages are versioned from the engine and move together. Pin them to the same exact version rather than a caret range. ## Bundler setup `@ferscloud/fers-calculation-web` is a WASM ES module that initialises via top-level `await`. Most bundlers need a one-time configuration change to allow that; the Node package needs none. ## Vite Add the plugin, then set a build target that supports top-level `await`. ```bash npm i -D vite-plugin-wasm ``` ## Vite config ```ts // vite.config.ts import wasm from "vite-plugin-wasm"; // The package initialises with a top-level await, so the build target // has to be one that supports it. esnext needs no second plugin. export default { plugins: [wasm()], build: { target: "esnext" }, }; ``` ## Next.js and webpack Transpile the package and turn on async WebAssembly. `asyncFunction` is required because the module initialises with a top-level await. ```js // next.config.js module.exports = { transpilePackages: ["@ferscloud/fers-calculation-web"], webpack(config) { config.experiments = { ...config.experiments, asyncWebAssembly: true }; config.output.environment = { ...config.output.environment, asyncFunction: true, }; return config; }, }; ``` > The solver is synchronous and CPU-bound. For large models, run it inside a Web Worker so the UI thread stays responsive. ## Solve a model No `init()` call and no API key. The free tier covers any model up to 100 members. ```ts import { calculate_from_json } from "@ferscloud/fers-calculation"; // No init() call needed — the nodejs and bundler builds initialise the // WASM module automatically on import. const res = JSON.parse(calculate_from_json(JSON.stringify(myModel))); if (res.ok) { const data = res.result; // displacements, member_results, unity_checks, … } else { console.error(res.error.code, res.error.message); } ``` ## The response envelope Every solver call returns a JSON **envelope**, so you parse once and branch on `ok` — you never have to sniff whether the returned string looks like an error. The return value is always valid JSON. ```jsonc // success { "ok": true, "result": { /* the full result document */ } } // failure (invalid model, over the member limit, malformed JSON, …) { "ok": false, "error": { "code": "LimitExceeded", "message": "Number of members (250) exceeds allowed maximum of 100" } } ``` | error.code | Means | | --- | --- | | `InvalidJson` | The model string was not parseable JSON. | | `LimitExceeded` | More members than the active tier allows (100 free, 10,000 Pro). | | `SolveError` | The model parsed but could not be solved — most often a singular stiffness matrix from a missing or under-specified support. | | `InternalPanic` | A bug in the engine. Please report it. | | `InternalSerialization` | The results could not be serialised. Also a bug worth reporting. | ## TypeScript types The browser package ships generated model types alongside the function signatures. They are generated from the engine's OpenAPI schema, so they track the published version rather than being maintained by hand. ```ts import type { FERS, ResultsBundle, } from "@ferscloud/fers-calculation-web/fers-models"; // FERS — the input model // ResultsBundle — the `result` payload of a successful envelope // Both are generated from the engine's OpenAPI schema, so they track // the published version. ``` ## Crediting FERS on the free tier Free-tier results carry an `attribution` object minted inside the solver. Pro results, solved with a valid token, do not — so the same code shows a credit on free and white-labels on Pro, with no flag to set. `getFersAttribution`, `fersAttributionText` and `createFersBadge` are exported alongside it. All of them accept the envelope, the parsed model or the results bundle, and all return nothing for a Pro result or an error envelope. ```ts import { fersAttributionHtml } from "@ferscloud/fers-calculation-web/badge.js"; // Free-tier results carry result.attribution; this renders the credit. // With a Pro solve token the field is absent and this returns "" — the // same code white-labels, with no flag to set. const credit = fersAttributionHtml(res); ``` > If you ship the free tier in an application, please display the credit. It is the only thing the free tier asks of you. Available from engine 0.2.61. ## Pro limits with a solve token Pro limits are unlocked by passing a short-lived signed token issued by the FERS Cloud server. The token is verified inside the WebAssembly module against an Ed25519 public key baked into the binary, so it cannot be forged. Your server holds the API key and exchanges it for a 30-minute token; the browser only ever sees the token. Keep `FERS_API_KEY` server-side — never in browser code. ```ts // pages/api/solve-token.ts const FERS_API_KEY = process.env.FERS_API_KEY!; let cached: { token: string; expiresAt: number } | null = null; export default async function handler(req, res) { const now = Date.now(); // Re-use if more than 5 minutes remain if (cached && cached.expiresAt - now > 5 * 60 * 1000) { return res.json({ token: cached.token }); } const resp = await fetch("https://ferscloud.com/api/solver/token", { method: "POST", headers: { "X-API-Key": FERS_API_KEY }, }); if (!resp.ok) return res.status(502).json({ error: "Token fetch failed" }); const { token, expiresAt } = await resp.json(); cached = { token, expiresAt: new Date(expiresAt).getTime() }; return res.json({ token }); } ``` ## Calling the solver with a token If the token is missing, expired or invalid, the solver falls back to the free member limit — it never throws. A genuine solve failure still comes back as `{ ok: false, error }` rather than an exception. ```ts import { calculate_from_json_with_token } from "@ferscloud/fers-calculation"; const { token } = await fetch("/api/solve-token").then((r) => r.json()); const res = JSON.parse( calculate_from_json_with_token(JSON.stringify(myModel), token), ); if (!res.ok) throw new Error(`${res.error.code}: ${res.error.message}`); const data = res.result; ``` | | Free | Pro | | --- | --- | --- | | Max members | 100 | 10,000 | | Function | `calculate_from_json` | `calculate_from_json_with_token` | | Requires a token | No | Yes | | Token lifetime | — | 30 minutes | ## Deflected shape Set `include_member_deflected_shape` in the model's analysis options to get a ready-to-plot, load-exact deflected shape per member — the member's global displacement sampled along its length — instead of reconstructing the curve yourself. It is off by default to keep the payload lean, and omitted from `member_results` when not requested. ```ts const model = { /* … model + load cases … */ analysis: { /* … */ options: { /* … */ include_member_deflected_shape: true, }, }, }; const res = JSON.parse(calculate_from_json(JSON.stringify(model))); const mr = res.result.results.loadcases["…"].member_results["1"]; // mr.member_displacements: [{ x_frac, displacement: [dx, dy, dz] }, …] // x_frac 0–1 along the member; displacement in the global input frame. ``` ## Other ways in - [Why solve structures in JavaScript](https://ferscloud.com/structural-analysis-javascript) — the case for running the solver client-side. - [REST API](https://ferscloud.com/docs/rest-api) — call the solver over HTTP from any language. - [MCP server](https://ferscloud.com/mcp) — let Claude, ChatGPT, Cursor or VS Code drive FERS directly. - [Python package](https://ferscloud.com/docs/installation) — the same engine, from a script. ## Frequently asked questions **Do I need an API key to solve in the browser?** No. `calculate_from_json` works with no key and no account for models up to 100 members. A key is only needed to mint the Pro solve token. **Does the model leave the browser?** No. The solver is compiled to WebAssembly and runs on the client, so the model and the results stay on the user's machine. Nothing is sent to FERS Cloud. **Why does Vite need extra configuration?** The browser package is a WASM ES module that initialises with a top-level `await`. `vite-plugin-wasm` handles the WebAssembly import, and `build.target: "esnext"` keeps that `await` in the output — which needs Chrome, Edge or Firefox 89+, or Safari 15+. webpack needs `asyncWebAssembly` and `asyncFunction` in its experiments and output settings. If `vite-plugin-top-level-await` is still in your config from an earlier version of this page, you can drop it: recent `@swc/core` releases make it fail at `vite build`. **Should I run the solver in a Web Worker?** For anything beyond a handful of members, yes. The solver is synchronous and CPU-bound, so calling it on the UI thread will block rendering for the duration of the solve. **Do I need an init() call?** No. Both the Node and bundler builds initialise the WebAssembly module automatically on import. Older documentation showed `await init()`; that is no longer required. **What happens if the solve token has expired?** The solver silently falls back to free-tier limits. It never throws on an invalid token, so a model within the free member limit keeps solving and a larger one returns `LimitExceeded`. **Which package do I use on the server?** `@ferscloud/fers-calculation`, built for the Node target. It needs no bundler configuration and self-initialises on import. **Are the results the same as the web app's?** Yes — it is the same Rust engine, compiled to a different target. The web app itself runs this WebAssembly build. ## Related - https://ferscloud.com/docs/rest-api - https://ferscloud.com/docs/quickstart --- # REST API reference > Every endpoint below is callable from any language over plain HTTP. Model quantities are SI base units — metres, newtons and pascals. The complete machine-readable spec is served as OpenAPI 3.1 and can be imported straight into a client generator or a ChatGPT Custom GPT Action. Source: https://ferscloud.com/docs/rest-api Last updated: 2026-09-04 ## The machine-readable spec The full specification lives at [ferscloud.com/api/openapi.json](https://ferscloud.com/api/openapi.json) in OpenAPI 3.1. It is served with permissive CORS and is explicitly allowed in `robots.txt`, so tools and agents can fetch it directly. Authenticate with either an `X-API-Key` header (a permanent key from your [Profile page](https://ferscloud.com/profile)) or a `Bearer` token from `POST /api/sdk/token`. The key form works for every sign-in method; the token form only for email-and-password accounts. ```bash # The complete machine-readable spec (OpenAPI 3.1). # Import it into ChatGPT Custom GPT Actions, Postman, or a client generator. curl https://ferscloud.com/api/openapi.json ``` ## Authentication and API keys Create a key once, store it server-side, and send it as `X-API-Key` on every request. Keys are shown once at creation and can be revoked at any time. ### `POST /api/sdk/token` — Get a short-lived JWT ```json { "description": "Exchange email + password for a 1-hour Bearer token. Requires an email/password account. Google/GitHub users should use an API key (X-API-Key header) instead.", "request": { "method": "POST", "url": "https://ferscloud.com/api/sdk/token", "body": { "email": "you@example.com", "password": "your_password" } }, "response": { "token": "", "expires_at": "2025-01-01T01:00:00Z", "is_premium": false, "user": { "id": "", "email": "you@example.com" } } } ``` ### `GET /api/sdk/me` — Verify a key and get user info ```json { "description": "Check the currently authenticated SDK user. Works with both X-API-Key and Bearer token.", "request": { "method": "GET", "url": "https://ferscloud.com/api/sdk/me", "headers": { "X-API-Key": "." } }, "response": { "user_id": "", "email": "you@example.com", "is_premium": false, "token_expires_at": "2026-01-01T01:00:00.000Z" } } ``` ### `POST /api/sdk/api-keys` — Create a persistent API key ```json { "description": "Create a named API key (returned once, store it securely). Optionally set expiresInDays for auto-expiry.", "request": { "method": "POST", "url": "https://ferscloud.com/api/sdk/api-keys", "headers": { "X-API-Key": "." }, "body": { "name": "My laptop", "scopes": [], "expiresInDays": null } }, "response": { "id": "", "name": "My laptop", "key": ".", "scopes": [], "created_at": "2026-01-01T00:00:00.000Z", "expires_at": null, "message": "Store this key securely. It will not be shown again." } } ``` ### `GET /api/sdk/api-keys` — List API keys ```json { "description": "List all active (non-revoked) API keys. Secrets are never returned.", "request": { "method": "GET", "url": "https://ferscloud.com/api/sdk/api-keys", "headers": { "X-API-Key": "." } }, "response": { "keys": [ { "id": "", "name": "My laptop", "scopes": [], "createdAt": "2026-01-01T00:00:00.000Z", "lastUsedAt": null, "expiresAt": null } ] } } ``` ### `DELETE /api/sdk/api-keys?id=` — Revoke an API key ```json { "description": "Revoke an API key by its ID. Pass the key ID as a query parameter, not a path segment.", "request": { "method": "DELETE", "url": "https://ferscloud.com/api/sdk/api-keys?id=", "headers": { "X-API-Key": "." } }, "response": { "message": "API key revoked" } } ``` ## Solver `solve` and `check-beam` are metered: each successful call uses one of your 100 free weekly solves, then prepaid credit at €0.01 per solve. `validate` is free and unmetered. These are the REST twins of the MCP `solve_model` and `check_beam` tools, so an agent and a script hit exactly the same code path. ### `POST /api/sdk/solve` — Run the FEM solver ```json { "description": "Solve a FERS model JSON (SI units: metres, newtons, pascals) and get displacements, member forces and reactions. Metered: uses your weekly free solves or prepaid credits (Pro: unlimited). REST twin of the MCP solve_model tool. Errors: 422 solver_error, 402 insufficient_credit, 429 daily_limit / rate_limited, 409 in_flight (same idempotency_key already running).", "request": { "method": "POST", "url": "https://ferscloud.com/api/sdk/solve", "headers": { "X-API-Key": "." }, "body": { "model": "", "idempotency_key": "optional-unique-id" } }, "response": { "result": "", "meta": { "funding": "free_tier", "cost_cents": 0, "balance_cents": 0, "replayed": false } } } ``` ### `POST /api/sdk/validate` — Check a model without solving ```json { "description": "Validate a FERS model JSON's structure (required keys, basic integrity) without running the solver. Free and unmetered.", "request": { "method": "POST", "url": "https://ferscloud.com/api/sdk/validate", "headers": { "X-API-Key": "." }, "body": { "model": "" } }, "response": { "valid": false, "errors": [ "Missing required key: \"model.materials\"" ] } } ``` ### `POST /api/sdk/check-beam` — Turnkey EN 1993-1-1 steel member check ```json { "description": "Build a single-span steel beam, solve it, and return the EN 1993-1-1 utilizations (bending, shear, N+M, lateral-torsional buckling). Units: span_m in metres, udl in kN/m, point_load in kN (downward). Counts as one solve. REST twin of the MCP check_beam tool. The example below reproduces the published IPE 400 worked example.", "request": { "method": "POST", "url": "https://ferscloud.com/api/sdk/check-beam", "headers": { "X-API-Key": "." }, "body": { "span_m": 7.5, "section": "IPE400", "material": "steel_S355", "udl": 13, "uls_factor": 1.35, "restrained": false } }, "response": { "check": { "section": "IPE400", "span_m": 7.5, "status": "Yellow", "governing_utilization": 0.822, "governing_check": "LTB (6.3.2)", "passes": true, "checks": { "Bending z (6.2.5)": 0.266, "Shear y (6.2.6)": 0.071, "Combined N+M (6.2.1)": 0.266, "LTB (6.3.2)": 0.822 }, "advice": "Passes (UC 0.822) with lateral-torsional buckling governing. …" }, "meta": { "funding": "free_tier", "cost_cents": 0, "balance_cents": 0, "replayed": false } } } ``` ## A complete call The turnkey beam check is the fastest way to confirm your key works — it builds, solves and checks a single span in one request. ```bash curl -X POST https://ferscloud.com/api/sdk/check-beam \ -H "X-API-Key: ." \ -H "Content-Type: application/json" \ -d '{ "span_m": 7.5, "section": "IPE400", "material": "steel_S355", "udl": 13, "uls_factor": 1.35, "restrained": false }' ``` > Units for this endpoint are pragmatic rather than SI: `span_m` in metres, `udl` in kN/m and `point_load` in kN downward. The model endpoints take SI throughout. ## Saved models Cloud model storage is a Pro feature. On the free tier, keep models as local JSON — the same format these endpoints accept and return. ### `GET /api/sdk/models` — List saved models ```json { "description": "List all models saved to the authenticated account", "request": { "method": "GET", "url": "https://ferscloud.com/api/sdk/models", "headers": { "X-API-Key": "." } }, "response": { "models": [ { "id": "", "name": "My cantilever", "description": null, "createdAt": "2025-01-01T00:00:00Z", "updatedAt": "2025-01-01T00:00:00Z" } ] } } ``` ### `POST /api/sdk/models` — Save a new model ```json { "description": "Save a structural model JSON to your account", "request": { "method": "POST", "url": "https://ferscloud.com/api/sdk/models", "headers": { "X-API-Key": "." }, "body": { "name": "My cantilever", "description": "5 m IPE 180 cantilever with −1 kN tip load", "model": "" } }, "response": { "id": "", "name": "My cantilever", "description": "5 m IPE 180 cantilever with −1 kN tip load", "createdAt": "2025-01-01T00:00:00Z", "updatedAt": "2025-01-01T00:00:00Z" } } ``` ### `GET /api/sdk/models/{id}` — Download a model ```json { "description": "Retrieve the full model JSON for a saved model", "request": { "method": "GET", "url": "https://ferscloud.com/api/sdk/models/", "headers": { "X-API-Key": "." } }, "response": { "id": "", "name": "My cantilever", "description": null, "model": "", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } } ``` ### `PUT /api/sdk/models/{id}` — Update a model ```json { "description": "Update the name, description, and/or JSON of an existing model. Send only the fields you want to change.", "request": { "method": "PUT", "url": "https://ferscloud.com/api/sdk/models/", "headers": { "X-API-Key": "." }, "body": { "name": "Updated name" } }, "response": { "id": "", "name": "Updated name", "description": null, "createdAt": "2025-01-01T00:00:00Z", "updatedAt": "2025-01-02T00:00:00Z" } } ``` ### `DELETE /api/sdk/models/{id}` — Delete a model ```json { "description": "Permanently delete a saved model", "request": { "method": "DELETE", "url": "https://ferscloud.com/api/sdk/models/", "headers": { "X-API-Key": "." } }, "response": "204 No Content" } ``` ## Error responses Metered endpoints use HTTP status codes to distinguish the reasons a call did not run, so a client can retry intelligently rather than treating every failure alike. | Status | Code | Means | | --- | --- | --- | | 422 | `solver_error` | The model reached the solver and could not be solved. | | 402 | `insufficient_credit` | Free quota exhausted and no prepaid credit left. | | 429 | `daily_limit` / `rate_limited` | Quota or rate limit hit. Back off and retry. | | 409 | `in_flight` | A request with the same `idempotency_key` is already running. | > Pass an `idempotency_key` on `solve` so a retried request is replayed rather than charged twice; the response `meta.replayed` tells you which happened. ## Other ways in - [MCP server](https://ferscloud.com/mcp) — the same capabilities as tools an AI agent can call directly. - [JavaScript package](https://ferscloud.com/docs/javascript) — solve in the browser with no HTTP call at all. - [Python package](https://ferscloud.com/docs/installation) — build models with objects rather than raw JSON. ## Frequently asked questions **Which authentication should I use?** An API key (`X-API-Key`) for anything automated. The `POST /api/sdk/token` route only works for accounts created with an email and password, and its token expires after an hour. **What units does the model JSON use?** SI base units: metres, newtons and pascals; angles in degrees. The `check-beam` convenience endpoint is the exception — it takes metres, kN/m and kN. **How much does a call cost?** 100 successful solves per rolling week are free. Beyond that it is €0.01 per solve from prepaid credit, or unlimited on Pro at €19.95/month. `validate` and the key endpoints are never metered. **Can I import this into ChatGPT or Postman?** Yes. [/api/openapi.json](https://ferscloud.com/api/openapi.json) is a complete OpenAPI 3.1 document, designed to be imported as a ChatGPT Custom GPT Action or into any OpenAPI toolkit. **How do I avoid being charged twice for a retry?** Send an `idempotency_key` with the solve request. A repeat of the same key returns the stored result with `meta.replayed: true` instead of solving again, and a concurrent repeat returns 409. **Is there a rate limit?** Yes, per account and per minute, to stop a runaway agent loop. Exceeding it returns 429 with `rate_limited`; back off and retry. ## Related - https://ferscloud.com/docs/javascript - https://ferscloud.com/docs/python-examples --- # Keyboard shortcuts > The modelling workspace is built around single-key tool switching, so most of a model can be drawn without leaving the viewport. Press **H** or **?** in the app to bring the same list up over your model. Source: https://ferscloud.com/docs/keyboard-shortcuts Last updated: 2026-09-04 ## Keyboard On macOS, Ctrl means Cmd. Tool keys are single presses with no modifier, so they work whenever the viewport has focus and you are not typing in a field. ### File | Shortcut | Action | | --- | --- | | `Ctrl+S` | Save | | `Ctrl+N` | New model | ### Edit | Shortcut | Action | | --- | --- | | `Ctrl+Z` | Undo | | `Ctrl+Y / Ctrl+Shift+Z` | Redo | | `Delete` | Delete selection | | `Ctrl+C` | Copy selection | | `Ctrl+Shift+C` | Copy (pick reference node) | | `Ctrl+V` | Paste | | `Ctrl+D` | Duplicate selection | | `Esc` | Cancel / clear selection | ### Selection | Shortcut | Action | | --- | --- | | `Ctrl+A` | Select all members | ### Tools | Shortcut | Action | | --- | --- | | `S` | Select mode | | `N` | Add node | | `M` | Add member | | `V` | Move (grab) node | | `C` | Assign section (click members) | | `L` | Add load (click node or member) | | `D` | Add uniform load (click member) | | `O` | Add moment (click node or member) | | `U` | Add support (click node) | ### View | Shortcut | Action | | --- | --- | | `P` | Cycle snap plane | | `G` | Toggle grid snap | ### Help | Shortcut | Action | | --- | --- | | `H / ?` | Keyboard shortcuts | ## Mouse Drag direction decides which selection rule applies, following the convention most CAD tools use: drag right and anything the box touches is selected; drag left and only fully enclosed objects are. ### Pointer | Shortcut | Action | | --- | --- | | `Left click` | Select object | | `Ctrl + Left click` | Multi-select | | `Drag right` | Crossing select (anything touched) | | `Drag left` | Window select (fully enclosed) | | `Scroll` | Zoom | > Snapping is what makes single-key drawing practical: **P** cycles the plane you are drawing in and **G** toggles snapping to the grid. ## Frequently asked questions **Do the shortcuts work on macOS?** Yes. Wherever Ctrl is listed, use Cmd. The single-key tool shortcuts are identical on both platforms. **Can I see this list inside the app?** Press **H** or **?** with the viewport focused, or open **Help (?) → Keyboard shortcuts**. The in-app panel is generated from the same registry as this page. **Why does a tool key do nothing sometimes?** Single-key shortcuts are suppressed while a text field has focus, so typing a name does not switch tools. Click into the viewport or press Esc first. **What is the difference between dragging left and right?** Dragging right is a crossing select — anything the box touches is picked. Dragging left is a window select — only objects entirely inside the box are picked. ## Related - https://ferscloud.com/docs/quickstart