Docs / Partners
Quoting from drawings
Send a PDF plan set or a 3D model and get back the building framed in steel, as a model you can show, plus the quote.
POST /quotes needs the building as numbers: floor area, storeys, wall run, roof, openings. Your app may have something better: the architectural drawings themselves, or a 3D model. POST /api/v1/plans takes those and does the rest, the way the estimator widget does on a fabricator's site. Gage reads the drawings, frames the building in cold-formed steel, prices it across the fabricator network at your margin, and answers with three things:
- The quote, the same object
POST /quotesreturns, with aquote_idyou reserve, send and accept like any other. - The design as read: floor area, storeys, wall run, roof, the confidence of the read, and every assumption the price rests on.
- The frame: a GLB of the steel frame and a massing model of the building to show your customer, and the steel take-off counted from the framed members. Once the job is accepted and the Gage fabricator has confirmed it, the build data follows: the building as a Pascal scene (members, walls, openings, levels, roof) and the panel schedule. See Building with Gage fabricators.
#What you can send
| File | How it is read |
|---|---|
| PDF plan set | Page by page: the text layer for dimension chains and schedules, images of the drawing sheets for the outline. Needs floor plans; elevations improve the roof and heights. |
| PNG, JPEG, WebP | As a drawing sheet. One sheet per file. |
Pascal build (.json) or Pascal GLB export | Walls, openings, levels and roof exactly as modelled. No reading, no confidence score. |
| IFC | Converted to a Pascal scene; walls and openings are the file's own. |
| glTF or GLB from anything else | Read by its envelope: footprint and height. Send model_height_ft to pin the scale. |
| Gage geometry JSON | The MCP server schema: levels, walls, openings, roof. |
Plan sets up to 15 MB and models up to 50 MB. A file under 4 MB goes inline; anything larger goes through an upload first (below).
#Sending a file
Multipart, with the build site ZIP:
curl -X POST https://buildwithgage.com/api/v1/plans \
-H "Authorization: Bearer $GAGE_API_KEY" \
-F "file=@cedar-street.pdf" \
-F "zip=80202" \
-F 'lead={"external_id":"ek_1042","label":"Reyes / Cedar Street","customer":{"name":"Dana Reyes","email":"dana@example.com"}}'
Or a JSON body pointing at a public https file:
{ "url": "https://files.ekstra.app/plans/cedar-street.pdf", "zip": "80202", "lead": { "external_id": "ek_1042" } }
Fields, all optional but zip:
| Field | Purpose |
|---|---|
zip | The build site. Drives routing, transport and the framing jurisdiction. |
type | residential, commercial or adu. Otherwise read from the title block, else residential. |
gauge_spec | Steel gauge. Otherwise 18ga. |
min_confidence | 0 to 1, default 0.7. A read below it comes back as a review instead of a price. |
model_height_ft | The building's height, for a mesh whose scale is unknown. |
lead | The same lead block as POST /quotes: label, model, external_id, customer. |
In a multipart request, lead is sent as a JSON string (-F 'lead={"external_id":"…"}') and numbers as plain text. An Idempotency-Key header works as on POST /quotes.
#Files over 4 MB
Ask for an upload, PUT the bytes, then quote by path:
curl -X POST https://buildwithgage.com/api/v1/plans/uploads \
-H "Authorization: Bearer $GAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"cedar-street.pdf","size":11829304}'
# → { "upload_url": "https://…", "storage_path": "api/<account>/….pdf", "token": "…" }
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @cedar-street.pdf
curl -X POST https://buildwithgage.com/api/v1/plans \
-H "Authorization: Bearer $GAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"storage_path":"api/<account>/….pdf","zip":"80202"}'
The upload is read once and deleted. A path belongs to the account that asked for it.
#The answer
resolution says which of three things happened.
quoted (201): the drawings were read and priced.
{
"resolution": "quoted",
"quote": {
"quote_id": "qte_ab12cd34ef56gh78",
"status": "priced",
"fabricator": { "id": "fab_op_7c2e91a4", "name": "Mountain West Fabricator", "location": "UT", "state": "UT", "lead_time_days": 21 },
"pricing": { "steel": 24900.00, "transport": 3036.00, "engineering": 9400.00, "subtotal": 37336.00, "range": { "low": 33602.00, "high": 41070.00 }, "confidence_band_pct": 10, "valid_until": "2026-10-04T18:02:11.000Z" },
"wholesale": { "subtotal": 33942.00 },
"markup_pct": 10,
"line_items": [],
"estimated_tons": 5.7,
"truck_count": 1
},
"design": {
"source": "document",
"sqft": 1200, "stories": 1, "wall_linear_ft": 140, "interior_wall_ft": 96,
"roof": { "type": "gable", "pitch": 6 },
"structure_type": "residential", "basement": false,
"confidence": 0.86, "confidence_label": "high",
"notes": ["Ground floor traced from sheet A-101: 40 × 30 ft, 4 corners.", "Roof 6:12 gable from the front elevation."]
},
"frame": {
"released": false,
"release": "The framed model and the steel frame GLB are released once the job is accepted and the Gage fabricator has confirmed it…",
"url": "/api/v1/quotes/qte_ab12cd34ef56gh78/frame",
"model_url": "/api/v1/quotes/qte_ab12cd34ef56gh78/model",
"summary": { "levels": 1, "storeys": 1, "lofts": 0, "panels": 14, "studs": 212, "headers": 9, "track_ft": 560, "footprint": "traced", "roof": { "type": "gable", "pitch": 6, "trusses": 21 }, "interior_wall_ft": 96 },
"flags": [],
"note": "Outline traced from the dimensioned floor plan.",
"massing": { "url": "/api/v1/quotes/qte_ab12cd34ef56gh78/frame?view=massing", "summary": { "storeys": 1, "width_ft": 40, "depth_ft": 30, "ridge_height_ft": 19 } },
"scene": null,
"cfs": { "studs": 212, "track_ft": 560, "joists": 0, "trusses": 21, "headers": 9, "steel_tons": 5.7, "jurisdiction": "CO", "panels": 14, "warnings": [] }
},
"assumptions": { "gauge_spec": "18ga" },
"consistency": null,
"disclaimer": "Preliminary quote generated from the design supplied…"
}
quote is exactly the POST /quotes body. frame.url is the frame as a GLB, frame.massing.url the approximate building shape, frame.model_url the frame as JSON; all three need your API key, so proxy them through your own server for a browser. Until a Gage fabricator has confirmed the job, frame.released is false and frame.scene is null; see The frame later. frame.cfs is the take-off counted from the members that were actually framed, which is what summary reports too. assumptions lists each priced field the drawings did not give, with the value used. frame_error is a sentence when the design read and priced but would not frame; frame is then null.
review (200): the drawings were read, but not well enough to stand behind a number. review.reason says why; quote is null. The frame's counts are still returned inline when there was geometry to draw, without URLs, since nothing was stored. Send it again with a lower min_confidence to price it anyway, or price the building with POST /quotes from what design read.
{
"resolution": "review",
"review": { "reason": "The design was read with 58% confidence, below the 70% this request requires for an automatic quote.", "what_happens_next": "…" },
"quote": null,
"design": { "source": "document", "sqft": 1140, "stories": 1, "confidence": 0.58, "confidence_label": "medium", "notes": ["…"] },
"frame": { "released": false, "url": null, "model_url": null, "summary": {}, "scene": null, "cfs": {}, "massing": { "url": null, "summary": {} } },
"disclaimer": "…"
}
reading (202): a large plan set needs more than one request. reading.file_id, reading.mime and reading.filename come back; send the same request again with those three fields in place of the file after retry_after_s seconds, and the read resumes from where the drawings were uploaded. Two or three rounds is normal for a thick set.
{
"resolution": "reading",
"reading": { "retry_after_s": 5, "file_id": "file_011CS…", "mime": "application/pdf", "filename": "cedar-street.pdf", "message": "Still reading the drawings…" },
"quote": null, "design": { "source": "document", "sqft": null, "notes": [] }, "frame": null
}
curl -X POST https://buildwithgage.com/api/v1/plans \
-H "Authorization: Bearer $GAGE_API_KEY" -H "Content-Type: application/json" \
-d '{"file_id":"file_011CS…","mime":"application/pdf","filename":"cedar-street.pdf","zip":"80202","lead":{"external_id":"ek_1042"}}'
Errors use the standard envelope. unreadable_design and the intake codes (no_levels, invalid_json, invalid_glb…) mean the file could not be read at all; unsupported_design means it was read but is outside what the engine prices (over four storeys, under 100 or over 50,000 sqft a floor).
#The frame later
GET /api/v1/quotes/{quote_id}/frame?view=massing returns the building shape and GET /api/v1/quotes/{quote_id}/model the take-off, summary and design as JSON, at any time. They are rebuilt from what the quote keeps, so they match what POST /plans returned. A quote made from parameters has none of these and answers 404 no_frame.
The build data waits for the job. Once the quote is accepted and the Gage fabricator has confirmed it by paying the fee (the moment GET /jobs/{job_id} reports fabricator.confirmed: true, or the job.status_changed webhook says so), /model carries frame.released: true with the Pascal scene. The steel frame GLB at /frame is there from the start, to show your customer.
#Showing the frame
There is no hosted viewer: the frame is data for your own app, in two forms with the same geometry: the GLB, and (once the job is confirmed) the scene.
The GLB at frame.url is a standard glTF 2.0 binary. Every framing member is a mesh: studs, track, headers, jambs, cripples, sills, joists, rafters, trusses, ridge, hip and valley members, in galvanized grey, with wall panels grouped by level. Units are metres, Y up, as glTF requires; plan north is −Z. Load it with three.js GLTFLoader, <model-viewer>, Babylon, or any glTF viewer. The bytes are deterministic for a quote, so cache them. frame.massing.url is the building's outer shape in the same conventions, useful as a first, lighter preview.
Because the URL needs your API key, serve it from your own server:
// app/api/frame/[quoteId]/route.ts (Next.js), or the equivalent on your stack
export async function GET(_req: Request, { params }: { params: { quoteId: string } }) {
const res = await fetch(`https://buildwithgage.com/api/v1/quotes/${params.quoteId}/frame`, {
headers: { authorization: `Bearer ${process.env.GAGE_API_KEY}` },
})
return new Response(res.body, { status: res.status, headers: { 'content-type': 'model/gltf-binary', 'cache-control': 'private, max-age=86400' } })
}
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
<model-viewer src="/api/frame/qte_ab12cd34ef56gh78" camera-controls auto-rotate shadow-intensity="1"></model-viewer>
The scene at frame.scene (and frame.model_url) is a Pascal scene graph: { nodes: { id: node }, rootNodeIds: [...] }, every node with id, type, parentId, name and children. Units are metres; plan y maps to −z. The node types you will meet:
type | Fields | Meaning |
|---|---|---|
site | polygon | The lot outline the building sits on. |
building | position, rotation | One per scene. |
level | level (index), metadata.gage.kind (storey or loft), metadata.gage.elevationFt | A floor. Lofts are platforms inside the roof, not storeys. |
wall | start, end (x, z), thickness, height, frontSide, metadata.gage.exterior, metadata.gage.bearing | A wall centreline. |
door, window | position[0] (centre along the wall from its start), position[1] (centre height), width, height | Openings, children of their wall. |
slab | polygon, holes, metadata.gage.porch | A floor plate, or a porch deck. |
roof / roof-segment | position, rotation, width, depth, pitch (degrees), overhang, wallHeight, roofType (gable, hip, shed) | The roof as wings over the storey whose plate carries the eave. |
Pascal's own viewer renders this directly, and it is what the Gage widget draws. If you only need the picture, use the GLB; if you need to reason about walls and openings, or let the customer edit the design, use the scene.
frame.cfs and frame.summary are the counts: studs, track feet, joists, trusses, rafters, headers, panels and steel tons, from the members that were actually framed, plus the framing engine's warnings.
#What is and is not priced
The price is the network's, by floor area, transport and engineering tier, exactly as POST /quotes prices it; the frame is what the drawings show. So the frame can include interior partitions and a porch that the per-square-foot package rate does not itemise. design.interior_wall_ft and frame.summary tell you what was drawn; the fabricator's firm quote after review is where those are priced.
Load zone is not read from drawings. If the site's snow, wind or seismic class matters to you, quote with POST /quotes and pass load_zone.
#Reading costs
Reading a plan set runs a vision model against every sheet, so this is the one call on the API with a hard limit: 40 calls per key per hour, whatever the file type. Models (Pascal, IFC, glTF, geometry JSON) are read without a model call and answer in well under a second.
The shop that won the quote appears by its regional alias, never by name; see Fabricator identity.