Python SDK Quickstart
Create a project, upload a structural PDF, poll the job, and download IFC in under 30 lines of Python. Studio API access requires a Studio subscription. Eurocode check endpoints work on any plan.
pip install requests — uses stdlib + requests onlyFRAMEAI_API_KEY=fai_… — never hardcode your token/api/v1/jobs or /projects/:id/runsdone, fetch IFC/BOM/DXF from export_urlsInstall
No official frameai package yet — use requests directly. All examples here are self-contained.
pip install requests
Authentication
Mint a key at Settings → API Keys. Export it as an environment variable — never hardcode it.
import os, requests
TOKEN = os.environ["FRAMEAI_API_KEY"] # fai_…
BASE = "https://frameai-structural.polsia.app/api/v1"
HDR = {"Authorization": f"Bearer {TOKEN}"}
30-line quickstart
The complete flow: create project → upload PDF → poll → download IFC.
#!/usr/bin/env python3
"""FrameAI Python quickstart — complete pipeline in 30 lines."""
import os, time, requests
TOKEN = os.environ["FRAMEAI_API_KEY"]
BASE = "https://frameai-structural.polsia.app/api/v1"
HDR = {"Authorization": f"Bearer {TOKEN}"}
# 1. Create a project workspace
proj = requests.post(f"{BASE}/projects", headers=HDR, json={
"name": "Warehouse 2026", "national_annex": "NL"
}).json()["project"]
print(f"Project: {proj['id']} — {proj['name']}")
# 2. Upload a PDF and start a run
with open("warehouse.pdf", "rb") as f:
run = requests.post(
f"{BASE}/projects/{proj['id']}/runs",
headers=HDR,
files={"pdf": ("warehouse.pdf", f, "application/pdf")}
).json()
run_id = run["run_id"]
print(f"Run started: {run_id} → poll: {run['poll_url']}")
# 3. Poll until done (typically 60–120 s)
for attempt in range(120):
time.sleep(5)
status = requests.get(f"{BASE}/runs/{run_id}", headers=HDR).json()["run"]
print(f" [{attempt*5:3d}s] {status['status']}", end="\r")
if status["status"] in ("done", "failed"):
break
# 4. Download IFC
if status["status"] == "done":
ifc_url = status["export_urls"]["model_ifc"]
ifc = requests.get(f"https://frameai-structural.polsia.app{ifc_url}", headers=HDR)
with open(f"run-{run_id}.ifc", "wb") as out:
out.write(ifc.content)
print(f"\n✓ Saved run-{run_id}.ifc ({len(ifc.content)//1024} KB)")
u = status["utilisation_summary"]
print(f" Members: {u['member_count']} Passed: {u['passed_count']} Max η: {u['max_utilisation']:.0f}%")
else:
print(f"\n✗ Run failed: {status['error']}")
Upload a PDF
Two ways: multipart upload (file on disk) or URL reference (file already in cloud storage).
Multipart upload
with open("drawing.pdf", "rb") as f:
r = requests.post(f"{BASE}/jobs", headers=HDR,
files={"pdf": ("drawing.pdf", f, "application/pdf")})
r.raise_for_status()
job_id = r.json()["job_id"]
URL reference
r = requests.post(f"{BASE}/jobs", headers=HDR, json={
"pdf_url": "https://my-bucket.s3.amazonaws.com/drawing.pdf",
"project_id": 42, # optional — group into a project
})
job_id = r.json()["job_id"]
Poll until done
The pipeline takes 60–120 seconds for a typical drawing. Poll every 5 seconds — don't hammer at sub-second intervals or you'll hit the rate limit.
import time, requests
def wait_for_job(job_id, *, timeout=300, interval=5):
"""Poll /jobs/:id until done or failed. Raises on timeout."""
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(f"{BASE}/jobs/{job_id}", headers=HDR)
r.raise_for_status()
job = r.json()["job"]
if job["status"] in ("done", "failed"):
return job
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not complete within {timeout}s")
job = wait_for_job(job_id)
if job["status"] == "failed":
raise RuntimeError(f"Job failed: {job['error']}")
Download IFC
Completed jobs expose export_urls with relative paths to each format.
def download_export(job, fmt):
"""Download shop.pdf | bom.xlsx | bom.csv | model.ifc | package.zip"""
urls = job.get("export_urls") or {}
key = fmt.replace(".", "_").replace("-", "_") # model.ifc → model_ifc
path = urls.get(key) or urls.get(fmt)
if not path:
raise KeyError(f"No export URL for {fmt}")
if path.startswith("/"):
path = "https://frameai-structural.polsia.app" + path
r = requests.get(path, headers=HDR)
r.raise_for_status()
return r.content
# Download all formats
ifc = download_export(job, "model_ifc")
xlsx = download_export(job, "bom_xlsx")
pdf = download_export(job, "shop_pdf")
with open("model.ifc", "wb") as f: f.write(ifc)
with open("bom.xlsx", "wb") as f: f.write(xlsx)
with open("shop.pdf", "wb") as f: f.write(pdf)
print("Exports downloaded.")
Use projects
Group related runs into a named project workspace. Each project can have its own National Annex for country-specific partial factors.
# Create a project
proj = requests.post(f"{BASE}/projects", headers=HDR, json={
"name": "Amsterdam Office Tower",
"description": "Phase 1 structural steel",
"national_annex": "NL", # NL | DE | FR | IT | BE | recommended
}).json()["project"]
# List projects
projects = requests.get(f"{BASE}/projects", headers=HDR).json()["projects"]
for p in projects:
print(f"{p['id']:4d} {p['name']}")
# Get project with its jobs
detail = requests.get(f"{BASE}/projects/{proj['id']}", headers=HDR).json()["project"]
print(f"Jobs: {len(detail['jobs'])}")
Error handling
All error responses have the shape { "error": "message" }. HTTP status codes follow REST conventions.
| Status | Meaning | Action |
|---|---|---|
| 400 | Bad request — missing field or invalid value | Fix the request body |
| 401 | Missing or invalid token | Check FRAMEAI_API_KEY is set and not expired |
| 402 | Plan restriction — Studio required | Upgrade at /pricing |
| 403 | Resource belongs to another account | Check you're using the right token |
| 404 | Not found | Verify the ID exists for your account |
| 409 | Conflict — job not in done status | Poll until done before downloading exports |
| 422 | No extracted data on the job | Re-upload a clearer drawing |
| 429 | Rate limited | Back off for Retry-After seconds (see below) |
| 500 | Server error | Retry after a few seconds; report if persistent |
def safe_get(url):
r = requests.get(url, headers=HDR)
if r.status_code == 402:
raise PermissionError("Studio plan required — upgrade at /pricing")
if r.status_code == 404:
raise LookupError(f"Not found: {url}")
r.raise_for_status()
return r.json()
Rate limit retry
The API allows 60 requests/minute per token. A 429 response includes a Retry-After header.
import time, requests
def get_with_retry(url, *, max_retries=3):
for attempt in range(max_retries + 1):
r = requests.get(url, headers=HDR)
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", 60))
if attempt < max_retries:
print(f"Rate limited — retrying in {wait}s")
time.sleep(wait)
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"Max retries exceeded for {url}")
Eurocode check API
Stateless per-endpoint checks. Free-tier: 10 calls/month (no auth needed). Pro: 1 000/month. Studio: unlimited.
import os, requests
# Eurocode checks work without auth on free tier (IP-keyed, 10/mo)
HDR_FREE = {} # unauthenticated
HDR_AUTH = {"Authorization": f"Bearer {os.environ['FRAMEAI_API_KEY']}"}
# EN 1993-1-8 bolted connection
r = requests.post(
"https://frameai-structural.polsia.app/api/v1/check/en-1993-1-8",
headers=HDR_AUTH,
json={
"bolt_grade": "8.8",
"bolt_size": "M20",
"shear_planes": 1,
"plate_fu": 360,
"plate_t": 10,
"e1": 60, "e2": 40, "p1": 80,
"is_end_bolt": True,
"V_Ed": 50000, # N per bolt
"F_t_Ed": 0,
}
)
result = r.json()
print(f"Passed: {result['passed']} η = {result['utilisation']:.1f}%")
print(f"Summary: {result['summary']}")
print(f"Clauses: {', '.join(result['eurocode_clauses_cited'])}")
# EN 1993-1-1 steel member
r = requests.post(
"https://frameai-structural.polsia.app/api/v1/check/en-1993-1-1",
headers=HDR_AUTH,
json={
"members": [{
"mark": "B1",
"profile": "IPE 300",
"length_mm": 5000,
"N_Ed": 0,
"M_y_Ed": 50, # kNm
"section_class": 1,
}],
"national_annex": "NL",
}
)
for m in r.json()["results"]:
print(f"{m['check_id']} passed={m['passed']} η={m['utilisation']:.1f}%")
GitHub example repo
A full runnable example — including a Grasshopper script, Jupyter notebook, and GitHub Actions CI workflow — is available in the FrameAI Examples collection.
Browse tutorials → · Full API reference → · Webhook event catalog → · OpenAPI 3.1 spec →