1. Create a workspace key
Sign in, select your workspace, and open Developer settings. An application and identity administrator must create the key. Name it “Grasshopper test”, choose a short expiry, select all projects, and enable projects:read, projects:write and models:read. Copy the secret when it is shown.
For this connection test, connect the copied secret directly to the ApiKey text input. The script uses that input first. Keep the key out of shared definitions. Use a workspace key, never a Clerk or Supabase service key.
Alternatively, leave ApiKey empty and set ONTOLOGIC_API_KEY before starting Rhino. Run the following in PowerShell, then launch Rhino from that same shell. Close the shell when finished.
$ghKey = Read-Host 'Workspace API key' -AsSecureString
$env:ONTOLOGIC_API_KEY = [System.Net.NetworkCredential]::new('', $ghKey).Password
# Adjust this path if Rhino is installed elsewhere.
& 'C:\Program Files\Rhino 8\System\Rhino.exe'2. Set up the component
Add a Python 3 Script component in Rhino 8 Grasshopper and paste the downloaded script. Rename its inputs and outputs below. Use Item access, a bool type hint for Run and str hints for the other inputs. Connect Run to a Button. Unused text inputs can stay empty.
| Input | Value |
|---|---|
Run | Button (false until pressed) |
ApiKey | Your copied workspace API key |
BaseUrl | https://www.ontologic.tools |
Action | whoami |
ProjectId | Empty initially; later a project UUID |
ModelId | Empty initially; later a model UUID |
Name | Grasshopper API test |
Description | Hello from Grasshopper |
OperationId | gh-text-test-20261004-01 (keep stable for retries) |
Create outputs Status, Data and ResultId, each connected to a Panel. Each press makes one request with a 15-second timeout. This synchronous connection test can briefly block Grasshopper while waiting.
3. Run these acceptance cases
| Action | Expected result |
|---|---|
| whoami | HTTP 200; workspace, environment, current role and scopes. |
| list_projects | HTTP 200; items and nextCursor. An empty list is valid. |
| create_project | HTTP 201; ResultId contains the new project UUID. This writes a real project. |
| create_project again, same text and OperationId | HTTP 200; replayed=true and the same UUID. No duplicate project. |
| read_project, with ResultId copied to ProjectId | HTTP 200; the exact Name and Description you sent. |
| list_models, with ProjectId | HTTP 200; revision metadata. A new project has items=[]. Choose an existing populated project to see models. |
| read_model, with ProjectId and ModelId | HTTP 200; filename, revision, original/upload status and provenance. Analysis may be unavailable. |
| create_project with changed text, same OperationId | HTTP 409 idempotency_conflict. Use a new OperationId for a new project. |
| whoami after revoking the key in Developer settings | HTTP 401. To continue, create a new key and update ApiKey (or restart Rhino with its environment). |
A 403 means missing scope or insufficient current role; 429 means wait according to Retry-After; 503 means configuration/provider availability needs attention. Report the request ID from Status. Collections return one page; this test does not follow nextCursor.
IFC ↔ Rhino 8 components
Current status: both converters, API routes and clients are integrated and tested locally. Production conversions await durable worker hosting and return 503 for authorized requests. Existing JSON connection tests remain available.
Download client package ZIP. Extract it to a local folder containing adapters/. No pip packages are needed in Rhino: the clients use Python’s standard library and RhinoCommon. Native IFC dependencies belong on the workers.
Add separate Python 3 Script components for receive and export. Use Item access unless indicated below. Text inputs use str hints, flags bool, BatchSize int. Use buttons for persistent actions and Panels for status. ApiKey takes the text input first, with ONTOLOGIC_API_KEY fallback. Keep keys out of saved/shared definitions. Toggle false before another action; recomputing true does not repeat it.
Receive → preview → bake
Paste the receive script. Use a verified immutable revision and a key with models:read and models:download. A viewer role is sufficient.
| Input | Setup |
|---|---|
AdapterRoot | Extracted client folder containing adapters/ |
BaseUrl | https://www.ontologic.tools |
ApiKey | Workspace key text or empty for environment fallback |
ProjectId / ModelId | Authorized project UUID / immutable model revision UUID |
IdempotencyKey | gh-receive-20261004-01; keep stable for retries |
Receive / Poll | Separate buttons: start / inspect and download after success |
Preview | Boolean toggle: true outputs meshes; cap 2,000 occurrences |
Bake / ContinueBake | Separate buttons: first / next explicit block batch |
Cancel | Button: cancel unfinished job or stop bake; completed batches remain |
RestoreOrigin | false for local origin; true restores IFC world coordinates |
BatchSize | Integer 100; accepted range 1–500 |
Outputs: Geometry (List meshes), IDs (List occurrence IDs), Metadata (typed records), Report (job/coverage/bake status), JobId, RhinoIDs (baked GUID list). Connect Geometry to a geometry parameter and Report/JobId to Panels.
Receive once → Poll until succeeded → Preview → Bake → ContinueBake until done. Cancel preserves completed batches; Bake again resumes/skips owned occurrences. Shared blocks retain element instances. Document units and unrelated objects are preserved. Save/reopen retains canonical document metadata and editable User Text.
Explicit selection → IFC4 → new revision
Paste the export script. Use models:write and a current editor/admin role; include models:read and models:download for results/publication. Jobs bind to their initiating key/user/project/environment.
| Input | Setup |
|---|---|
RepoPath | Extracted client folder containing adapters/ |
BaseUrl / ApiKey / ProjectId | Same HTTPS origin, workspace key and target project UUID |
Filename / Revision | selection.ifc / gh-r1 |
SelectedIds | List access, str hints: explicit object/instance GUID strings |
NestedPaths | List access, str hints: JSON arrays ["root GUID", "member GUID", ...]; empty for whole objects |
OutputDirectory | Writable local folder for unique Exchange ZIP and downloaded IFC |
IdempotencyKey | gh-export-20261004-01; stable for this package, new key for a new export |
AllowProxy | true explicitly maps untyped/unsupported classes to IfcBuildingElementProxy |
Export / Start / Poll | Buttons: extract selection / upload and queue / inspect |
Download / Upload / Cancel | Buttons: download validated IFC / publish new immutable revision / cancel |
Outputs: JobId, Status, Report, PackagePath, IfcPath, Model (published model JSON). Connect each to a Panel. Feed receive RhinoIDs into SelectedIds for imported instances, or supply explicit Rhino GUIDs. Empty selection never means select all.
Export → Start → Poll until succeeded → Download if wanted → Upload to publish. Upload reserves signed original storage, verifies hash/size and completes a NEW immutable IFC revision. It never replaces a prior IFC. Each distinct export needs a fresh operation key. Failed conversion uses a new operation; no export retry endpoint exists.
Geometry and metadata boundaries
Z-up metres, float64 placements, shared repeated/nested blocks, mirrors and nonuniform transforms are supported. Receive produces meshes. Export tessellates Mesh/Brep/Extrusion; curves, points, SubD and textures/UV fail explicitly. Editable authoring solids and parametric reconstruction are not promised.
Typed values, Unicode, lists, nested objects, nulls, namespaces, type/instance data and unknown User Text survive through document metadata and encoded IFC properties. Missing projections do not delete source data; edited projections are type checked. Continuing unique GlobalIds survive; copies get new identities with lineage. Coincident ambiguous copies get new IDs and a diagnostic. Source quantities remain provenance, not recomputed measurements.
Original IFC, export ZIP and generated IFC: 50 MiB. Control JSON: 16 KiB. Signed adapter transfers: 60 seconds, without workspace Authorization. Native parsing/generation stays in workers outside Vercel. General element/property/metric HTTP queries remain planned.
Component setup follows McNeel’s Grasshopper Python guide.
Python component source
"""Rhino 8 Python 3 inputs: Run, ApiKey, BaseUrl, Action, ProjectId, ModelId, Name,
Description, OperationId. Outputs: Status, Data, ResultId.
Connect ApiKey, or set ONTOLOGIC_API_KEY before Rhino. Reuse OperationId on retries.
"""
import json
import os
import uuid
from urllib.parse import urlsplit
from urllib.request import Request, build_opener, HTTPRedirectHandler
from urllib.error import HTTPError, URLError
class NoRedirects(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def api_request(base, key, action, project_id="", model_id="", name="",
description="", operation_id=""):
parsed = urlsplit(base)
local = parsed.hostname in ("localhost", "127.0.0.1", "::1")
if parsed.scheme != "https" and not (parsed.scheme == "http" and local):
raise ValueError("Use HTTPS, or HTTP on localhost for local testing.")
if not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("BaseUrl must be a site URL without credentials, query or fragment.")
if parsed.path not in ("", "/"):
raise ValueError("BaseUrl must not include /api/v1 or another path.")
if not key or any(c.isspace() for c in key):
raise ValueError("Supply ApiKey, or set ONTOLOGIC_API_KEY before starting Rhino.")
paths = {"whoami": "/whoami", "list_projects": "/projects"}
method, body = "GET", None
headers = {"Authorization": "Bearer " + key, "Accept": "application/json"}
if action == "create_project":
if not operation_id or len(operation_id) < 8:
raise ValueError("Supply a stable OperationId of 8–128 characters.")
paths[action] = "/projects"
method = "POST"
body = json.dumps({"name": name, "description": description}).encode("utf-8")
headers.update({"Content-Type": "application/json", "Idempotency-Key": operation_id})
elif action in ("read_project", "list_models", "read_model"):
project_id = str(uuid.UUID(str(project_id)))
paths[action] = "/projects/" + project_id
if action in ("list_models", "read_model"):
paths[action] += "/models"
if action == "read_model":
paths[action] += "/" + str(uuid.UUID(str(model_id)))
if action not in paths:
raise ValueError("Choose whoami, list_projects, create_project, read_project, list_models or read_model.")
request = Request(base.rstrip("/") + "/api/v1" + paths[action],
data=body, headers=headers, method=method)
try:
response = build_opener(NoRedirects()).open(request, timeout=15)
except HTTPError as error:
response = error
except (URLError, TimeoutError):
return "Connection failed or timed out. Check BaseUrl and network.", "", ""
with response:
status = response.code
try:
payload = json.loads(response.read(1024 * 1024).decode("utf-8"))
except (ValueError, UnicodeError):
return "HTTP {}: expected JSON; check canonical URL and deployment protection.".format(status), "", ""
if status < 200 or status >= 300:
error = payload.get("error", {})
return "HTTP {}: {} (request {})".format(status, error.get("code", "unexpected_response"),
error.get("requestId", "unknown")), "", ""
data = payload.get("data", {})
replay = response.headers.get("Idempotency-Replayed")
message = "HTTP {}".format(status) + ("; replayed=" + replay if replay else "")
return message, json.dumps(data, indent=2, ensure_ascii=False), data.get("id", "")
if "Run" in globals():
Status, Data, ResultId = "Press Run to send one request.", "", ""
if Run:
try:
Status, Data, ResultId = api_request(
BaseUrl or "https://www.ontologic.tools",
(globals().get("ApiKey") or "").strip() or os.environ.get("ONTOLOGIC_API_KEY", "").strip(),
Action or "whoami", ProjectId, ModelId, Name or "Grasshopper API test",
Description or "Text sent from Grasshopper", OperationId)
except ValueError as error:
Status = str(error)
except Exception:
Status = "Request failed. Check inputs and retry with the same OperationId."