# Handrail & Baluster API
> Railing and baluster layout maths as an API, computed locally and deterministically — the baluster-count, spacing and post numbers a deck builder, fabricator or balustrade designer sets a guardrail out with. The baluster-count endpoint gives the smallest number of balusters that keeps every gap within the safety limit: between two posts n balusters leave n+1 gaps, so the count = ceil((rail length − max gap) ÷ (baluster width + max gap)). The usual guardrail limit is a 100 mm (4-inch) sphere — a child-safety rule — so a 2000 mm rail with 40 mm balusters needs 14 of them at even 96 mm gaps; round up, because one fewer opens the gaps past the limit. The layout endpoint sets out a known count evenly: the gap = (rail length − total baluster width) ÷ (count + 1), the centre-to-centre pitch = baluster width + gap, and the first baluster's centre sits one gap plus half a baluster from the post face, so you mark the first centre and step off the pitch with the last gap landing equal to the first. The post-count endpoint sizes the frame: a run needs one more post than spans, spans = ceil(run ÷ max post spacing), posts = spans + 1, even spacing = run ÷ spans — a 6 m run at a 1.8 m max takes 4 spans and 5 posts at a tidy 1.5 m. Everything is computed locally and deterministically, so it is instant and private. Ideal for deck and balustrade design tools, fabrication and estimating apps, and building calculators. Pure local computation — no key, no third-party service, instant. Uses the common 100 mm infill rule — confirm your local code. 3 compute endpoints. For stair rise and run use a stair API; for fence pickets a fence API.

## Authentication
All requests require your oanor API key in the `x-oanor-key` header. Get one at https://www.oanor.com/developer/keys.

```bash
curl -H "x-oanor-key: oanor_live_…" "https://api.oanor.com/handrail-api/..."
```

## Pricing
- **Free** (Free) — 7,250 calls/Mo, 2 req/s
- **Starter** ($7/Mo) — 58,500 calls/Mo, 6 req/s
- **Pro** ($24/Mo) — 251,000 calls/Mo, 15 req/s
- **Mega** ($75/Mo) — 1,165,000 calls/Mo, 40 req/s

## Endpoints

### Railing

#### `GET /v1/baluster-count` — Balusters for a code-safe gap

**Parameters:**
- `railing_length_mm` (query, required, string) — Clear length between posts (mm) Example: `2000`
- `baluster_width_mm` (query, required, string) — Baluster width (mm) Example: `40`
- `max_gap_mm` (query, optional, string) — Maximum gap (mm, default 100) Example: `100`

**Example:**
```bash
curl -H "x-oanor-key: $KEY" \
  "https://api.oanor.com/handrail-api/v1/baluster-count?railing_length_mm=2000&baluster_width_mm=40&max_gap_mm=100"
```

**Response:**
```json
{
    "data": {
        "gaps": 15,
        "note": "Between two posts, n balusters leave n+1 gaps, and the smallest count that keeps every gap within the limit = ceil((length − max gap) ÷ (baluster width + max gap)). The usual guardrail limit is a 100 mm (4-inch) sphere — a child-safety rule — so a 2000 mm rail with 40 mm balusters needs 14 of them, leaving even 96 mm gaps. Always round the count up; one fewer baluster opens the gaps past the limit.",
        "inputs": {
            "max_gap_mm": 100,
            "baluster_width_mm": 40,
            "railing_length_mm": 2000
        },
        "code_ok": true,
        "actual_gap_mm": 96,
        "baluster_count": 14
    },
    "meta": {
        "timestamp": "2026-06-07T08:17:48.027Z",
        "request_id": "81d83dc8-15e3-427b-a515-a7758ddc2935"
    },
    "status": "ok",
    "message": "Baluster count",
    "success": true
}
```

#### `GET /v1/layout` — Even spacing for a count

**Parameters:**
- `railing_length_mm` (query, required, string) — Clear length between posts (mm) Example: `2000`
- `baluster_count` (query, required, string) — Number of balusters Example: `14`
- `baluster_width_mm` (query, required, string) — Baluster width (mm) Example: `40`

**Example:**
```bash
curl -H "x-oanor-key: $KEY" \
  "https://api.oanor.com/handrail-api/v1/layout?railing_length_mm=2000&baluster_count=14&baluster_width_mm=40"
```

**Response:**
```json
{
    "data": {
        "note": "To set out a known number of balusters evenly, the gap = (rail length − total baluster width) ÷ (count + 1), the centre-to-centre pitch = baluster width + gap, and the first baluster's centre sits one gap plus half a baluster from the post face. Mark the first centre, then step off the pitch — the last gap lands equal to the first, which is what makes a run look right.",
        "gap_mm": 96,
        "inputs": {
            "baluster_count": 14,
            "baluster_width_mm": 40,
            "railing_length_mm": 2000
        },
        "centre_to_centre_mm": 136,
        "first_centre_from_end_mm": 116
    },
    "meta": {
        "timestamp": "2026-06-07T08:17:48.118Z",
        "request_id": "f742ccdb-aeaf-4444-8581-9d8129272881"
    },
    "status": "ok",
    "message": "Layout",
    "success": true
}
```

#### `GET /v1/post-count` — Posts for a run

**Parameters:**
- `total_run_mm` (query, required, string) — Total railing run (mm) Example: `6000`
- `max_spacing_mm` (query, optional, string) — Max post spacing (mm, default 1800) Example: `1800`

**Example:**
```bash
curl -H "x-oanor-key: $KEY" \
  "https://api.oanor.com/handrail-api/v1/post-count?total_run_mm=6000&max_spacing_mm=1800"
```

**Response:**
```json
{
    "data": {
        "note": "A run of railing needs one more post than it has spans: spans = ceil(run ÷ max post spacing), posts = spans + 1, and the even spacing = run ÷ spans. Guard posts are typically held to ~1.8 m so the rail stays stiff and meets the in-fill load; a 6 m run at 1.8 m max takes 4 spans and 5 posts at a tidy 1.5 m each. Corners and gate posts are extra.",
        "spans": 4,
        "inputs": {
            "total_run_mm": 6000,
            "max_spacing_mm": 1800
        },
        "post_count": 5,
        "actual_spacing_mm": 1500
    },
    "meta": {
        "timestamp": "2026-06-07T08:17:48.211Z",
        "request_id": "279d7d97-c5de-4515-ad1e-086a18af7d38"
    },
    "status": "ok",
    "message": "Post count",
    "success": true
}
```

### Meta

#### `GET /v1/meta` — Spec

**Example:**
```bash
curl -H "x-oanor-key: $KEY" \
  "https://api.oanor.com/handrail-api/v1/meta"
```

**Response:**
```json
{
    "data": {
        "notes": "Millimetres. count = ceil((L−maxgap)/(width+maxgap)); gap = (L−count·width)/(count+1); posts = ceil(run/maxspacing)+1. 100 mm sphere is the usual infill limit. For stair rise/run use a stair API; for fence pickets a fence API.",
        "service": "handrail-api",
        "endpoints": {
            "GET /v1/meta": "This document.",
            "GET /v1/layout": "Even gap, pitch and first-centre for a baluster count.",
            "GET /v1/post-count": "Post count and spacing for a railing run.",
            "GET /v1/baluster-count": "Minimum balusters so every gap <= the limit."
        },
        "description": "Railing / baluster layout: minimum balusters for a code gap, even spacing of a count, and post count for a run."
    },
    "meta": {
        "timestamp": "2026-06-07T08:17:48.291Z",
        "request_id": "fc1ffbdc-f326-4a71-8e83-6d09bde3c871"
    },
    "status": "ok",
    "message": "Meta",
    "success": true
}
```


---
Marketplace page: https://www.oanor.com/api/handrail-api
OpenAPI spec: https://www.oanor.com/api/handrail-api/openapi.json
