SDK & REST API
Drop Wellbore Genius solvers into your notebook or pipeline. JSON over HTTPS, bearer-token auth, team-scoped. Mint a key under Settings → SDK API keys.
Every request must include an Authorization header with a bearer token minted from Settings. Tokens look like dh_live_… and are shown only once at mint time. Revoke under Settings; last_used_at bumps on every successful call.
Returns the full solver-spec scoreboard — every coupling kernel's "Today" vs "Roadmap" stance. Stable JSON shape, mirrors the in-app /solver-spec page.
curl https://wellboregenius.com/api/public/sdk/v1/solver-spec \
-H "Authorization: Bearer dh_live_..."Drives the non-planar 3D fracture pipeline (DDM + tip kinking + optional out-of-plane tilt and stress-driven curvature) on a synthetic rectangular mesh. Returns per-step history + final mesh summary.
curl -X POST https://wellboregenius.com/api/public/sdk/v1/non-planar-3d/run \
-H "Authorization: Bearer dh_live_..." \
-H "Content-Type: application/json" \
-d '{
"halfLengthFt": 300,
"halfHeightFt": 150,
"elementSizeFt": 30,
"ePrimePsi": 4500000,
"defaultNetPressurePsi": 350,
"defaultSigmaHPsi": 4200,
"advanceFt": 25,
"maxKinkDeg": 10,
"steps": 8
}'A pip install downhole wrapper will ship separately. In the meantime, a dependency-free typed client (stdlib only) is bundled at /sdk/python/downhole_sdk.py. It includes typed dataclasses + Bearer auth for parent_child_analyze(). Or call the API directly:
# pip install requests
import requests
API_KEY = "dh_live_..."
BASE = "https://wellboregenius.com/api/public/sdk/v1"
H = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def solver_spec():
r = requests.get(f"{BASE}/solver-spec", headers=H)
r.raise_for_status()
return r.json()
def non_planar_3d_run(**kwargs):
r = requests.post(f"{BASE}/non-planar-3d/run", headers=H, json=kwargs)
r.raise_for_status()
return r.json()
def parent_child_analyze(parents, child, reservoir, **kwargs):
payload = {"parents": parents, "child": child, "reservoir": reservoir, **kwargs}
r = requests.post(f"{BASE}/parent-child/analyze", headers=H, json=payload)
r.raise_for_status()
return r.json()
# Simulations CRUD ----------------------------------------------------------
def list_simulations(workspace_id=None, limit=200):
params = {"limit": limit}
if workspace_id:
params["workspaceId"] = workspace_id
r = requests.get(f"{BASE}/simulations", headers=H, params=params)
r.raise_for_status()
return r.json()["simulations"]
def create_simulation(workspace_id, name, **kwargs):
payload = {"workspaceId": workspace_id, "name": name, **kwargs}
r = requests.post(f"{BASE}/simulations", headers=H, json=payload)
r.raise_for_status()
return r.json()
def get_simulation(sim_id):
r = requests.get(f"{BASE}/simulations/{sim_id}", headers=H)
r.raise_for_status()
return r.json()
def update_simulation(sim_id, **patch):
r = requests.patch(f"{BASE}/simulations/{sim_id}", headers=H, json=patch)
r.raise_for_status()
return r.json()
def delete_simulation(sim_id):
r = requests.delete(f"{BASE}/simulations/{sim_id}", headers=H)
r.raise_for_status()
return r.json()
if __name__ == "__main__":
print(solver_spec()["count"], "scoreboard rows")
np3d = non_planar_3d_run(
halfLengthFt=300, halfHeightFt=150, elementSizeFt=30,
ePrimePsi=4_500_000, defaultNetPressurePsi=350,
defaultSigmaHPsi=4200, advanceFt=25, maxKinkDeg=10, steps=8,
)
print("np3D steps:", len(np3d["history"]), "final mesh:", np3d["finalMesh"])
pc = parent_child_analyze(
parents=[
{"id":"P1","heel":{"x":0,"y":660},"toe":{"x":10000,"y":660},"drawdownPsi":1000},
{"id":"P2","heel":{"x":0,"y":-660},"toe":{"x":10000,"y":-660},"drawdownPsi":1000},
],
child={"id":"C1","heel":{"x":0,"y":0},"toe":{"x":10000,"y":0},"stageCount":50},
reservoir={"biotAlpha":0.85,"poissonRatio":0.22},
)
print("parent-child worst stage:", pc["summary"]["worstStageId"])
sim = create_simulation("default", "bakken_baseline", tags=["history-match"])
update_simulation(sim["simulation"]["id"], status="completed",
runtimeSeconds=1843, results={"peakBhpPsi": 9120})
print("simulations on team:", len(list_simulations()))Dependency-free scripts driven by built-in Invoke-RestMethod. Set $Env:DOWNHOLE_API_KEY once, then run:
- /sdk/powershell/README.md — quickstart + auth.
- Invoke-SolverSpec.ps1 — GET /solver-spec.
- Invoke-ParentChildAnalyze.ps1 — POST /parent-child/analyze.
- Invoke-NonPlanar3dRun.ps1 — POST /non-planar-3d/run.
Every PowerShell example script hits one of the endpoints below. Field types, ranges, defaults, and required/optional flags are enforced server-side by Zod — a body that violates any rule comes back as 400 { error: { code: "bad_request", message } } with the exact failing path.
GET /api/public/sdk/v1/solver-spec (Get-SolverSpecReport, Invoke-SolverSpec; MATLAB: getSolverSpecReport.m)
No request body. Response is a stable envelope; the CSV writer in Get-SolverSpecReport.ps1 pins the column order.
// 200 OK
{
"scoreboard": [
{
"id": string, // e.g. "perf-erosion-clearance"
"title": string,
"status"?: "shipped" | "next-up", // omitted ≡ "shipped"
"coupling"?: string, // e.g. "Two-way, in Newton loop"
"newtonWired"?: boolean,
"newtonWiredDetail"?: string,
"today": string, // 1-2 sentence "what runs today"
"roadmap": string // 1-2 sentence "what's next"
}
],
"count": number, // = scoreboard.length
"generatedAt": string // ISO 8601 UTC
}POST /api/public/sdk/v1/parent-child/analyze (Invoke-ParentChildAnalyze)
Body — all objects .strict():
{
"parents": [ // 1..20 entries
{
"id": string, // 1..64 chars, required
"label"?: string, // ≤128 chars
"heel": { "x": number, "y": number }, // ft, finite
"toe": { "x": number, "y": number }, // ft, finite
"drawdownPsi": number, // 0..20 000
"drainageRadiusFt"?: number // >0, ≤20 000
}
],
"child": {
"id": string, // 1..64 chars
"label"?: string,
"heel": { "x": number, "y": number },
"toe": { "x": number, "y": number },
"stageCount": integer // 1..200
},
"reservoir": {
"biotAlpha": number, // 0..1.5
"poissonRatio": number // 0.01..0.49
},
"asymmetrySensitivity"?: number // 0..10, default engine value
}Response (200 OK):
{
"stages": [
{
"stageIndex": integer, // 0-based
"stageLabel": string, // "Stage N", 1-based
"midpoint": { "x": number, "y": number }, // ft
"depletionPsi": number, // psi
"dSigmaHPsi": number, // psi
"asymmetryPct": number, // -100..+100
"nearestParentId": string | null,
"nearestParentFt": number, // ft
"bashingRisk": "low" | "watch" | "high"
}
],
"summary": {
"highCount": integer,
"watchCount": integer,
"lowCount": integer,
"worstStageId": string | null, // stageLabel of max Δp
"worstDepletionPsi": number,
"meanDepletionPsi": number,
"meanDSigmaHPsi": number
},
"generatedAt": string
}POST /api/public/sdk/v1/non-planar-3d/run (Invoke-NonPlanar3dRun; MATLAB: runNonPlanar3d.m)
{
"halfLengthFt": number, // >0, ≤5 000
"halfHeightFt": number, // >0, ≤2 000
"elementSizeFt": number, // >0, ≤200
"ePrimePsi": number, // >0, ≤50 000 000
"defaultNetPressurePsi"?: number, // -5 000..20 000, default 300
"defaultSigmaHPsi"?: number, // 0..30 000, default 3 500
"advanceFt": number, // >0, ≤500
"maxKinkDeg": number, // >0, ≤45
"steps": integer, // 1..50
"sigmaHMaxAzimuthRad"?: number, // any finite; constant across steps
"outOfPlaneTiltDeg"?: number, // -45..45
"stressDrivenCurvatureEnabled"?: boolean,
"maxCurvatureDegPerFt"?: number // >0, ≤10 (only if stress-driven enabled)
}Response (200 OK):
{
"history": [ // length = steps
{
"step": integer,
"advanceFt": number,
"kinkAppliedDeg": number,
"meanApertureIn": number,
"meanNetPressurePsi": number,
"tipCount": integer
}
],
"finalMesh": {
"vertexCount": integer,
"faceCount": integer,
"boundingBoxFt": {
"min": { "x": number, "y": number, "z": number },
"max": { "x": number, "y": number, "z": number }
}
},
"generatedAt": string
}POST /api/public/sdk/v1/marketplace/bundle (Export-MarketplacePresets, Import-MarketplacePresets; MATLAB: exportMarketplacePresets.m, importMarketplacePresets.m)
Accepts a single envelope, a bare array, or a bundle object — all three shapes are normalized to { version: 1, entries }.
// Any of these three request shapes:
{ "version": 1, "entries": [ envelope, envelope, ... ] } // ≤500 entries
[ envelope, envelope, ... ] // bare array, ≤500
envelope // single object
// envelope shape (all objects .strict()):
{
"version": 1, // literal 1
"id": string, // 1..200 chars
"kind": // enum
| "basin.parent-child"
| "basin.pressure-advisor"
| "vendor.slurry"
| "vendor.breaker"
| "vendor.proppant"
| "calibration.net-pressure-knobs"
| "calibration.dfit"
| "other",
"title": string, // 1..200 chars
"description"?: string, // ≤2 000, default ""
"basin"?: string, // ≤120
"vendor"?: string, // ≤120
"tags"?: string[], // ≤32 items, each 1..60
"author": string, // 1..120
"publishedAtIso": string, // 1..64
"payload": unknown // kind-specific; opaque to server
}Response (200 OK):
{
"bundle": { "version": 1, "entries": envelope[] }, // canonical, id-deduped, ≤500
"report": {
"input": integer, // # candidates received
"accepted": integer, // # kept in bundle.entries
"rejected": integer, // # failed envelope validation
"deduped": integer, // # dropped as duplicate id
"overflow": integer // # trimmed by the 500 cap
},
"rejected": [ { "index": integer, "reason": string } ],
"generatedAt": string
}Common error envelope
// 400 bad_request | 401 unauthorized | 500 internal_error
{
"error": {
"message": string, // Zod flatten() JSON on 400
"code": "bad_request" | "unauthorized" | "internal_error" | "analyzer_error" | null
}
}CORS: every endpoint answers OPTIONS with 204 and the standard Access-Control-Allow-* headers, so browser callers work too.
MATLAB ↔ Wellbore Genius runs over the Python SDK via py.* (no MEX, no Java). Works on R2019b+ across Linux / macOS / Windows.
- /sdk/matlab/README.md —
pyenvsetup + hello-solver. - Repository examples under
examples/matlab/:run_parent_child_example.m,run_fracpro_export_example.m,run_end_to_end_example.m, and the reusablesimulink_parent_child_block.m. - Zero-dependency
webreadscripts that mirror the PowerShell workflow: setDownholeApiKey.m (auth, mirrorsSet-DownholeApiKey.ps1) and getSolverSpecReport.m (GET /solver-spec + JSON/CSV writer +FailOnNextUpCI gate, mirrorsGet-SolverSpecReport.ps1— CSV columns byte-comparable).
Runs the analytical parent–child interference engine. Returns per-stage Δp, Δσ_h, asymmetry %, nearest parent, and bashing-risk chip — plus the rolled-up summary.
curl -X POST https://wellboregenius.com/api/public/sdk/v1/parent-child/analyze \
-H "Authorization: Bearer dh_live_..." \
-H "Content-Type: application/json" \
-d '{
"parents": [
{"id":"P1","heel":{"x":0,"y":660},"toe":{"x":10000,"y":660},"drawdownPsi":1000},
{"id":"P2","heel":{"x":0,"y":-660},"toe":{"x":10000,"y":-660},"drawdownPsi":1000}
],
"child": {"id":"C1","heel":{"x":0,"y":0},"toe":{"x":10000,"y":0},"stageCount":50},
"reservoir": {"biotAlpha":0.85,"poissonRatio":0.22}
}'Response schema (200 OK)
stages[] — one entry per child stage, ordered heel→toe:
stageIndex(number) — 0-based index.stageLabel(string) — 1-based UI label, e.g."Stage 12".midpoint({x, y}) — stage centroid in plan view [ft].depletionPsi(number) — depletion-induced Δp at the stage [psi].dSigmaHPsi(number) — Eaton Δσ_h from depletion [psi].asymmetryPct(number, −100..+100) — signed half-length skew toward the more-depleted side.nearestParentId(string | null) — id of closest parent, or null if none.nearestParentFt(number) — perpendicular distance to nearest parent [ft].bashingRisk("low" | "watch" | "high") — frac-hit severity chip.
summary — roll-up across all stages:
highCount,watchCount,lowCount(number) — stages in each risk bucket.worstStageId(string | null) —stageLabelof the highest-Δp stage.worstDepletionPsi(number) — max Δp across stages [psi].meanDepletionPsi(number) — mean Δp across stages [psi].meanDSigmaHPsi(number) — mean Δσ_h across stages [psi].
Example response
{
"stages": [
{
"stageIndex": 0,
"stageLabel": "Stage 1",
"midpoint": { "x": 100, "y": 0 },
"depletionPsi": 312.4,
"dSigmaHPsi": 187.6,
"asymmetryPct": 4.2,
"nearestParentId": "P1",
"nearestParentFt": 660,
"bashingRisk": "watch"
},
{
"stageIndex": 1,
"stageLabel": "Stage 2",
"midpoint": { "x": 300, "y": 0 },
"depletionPsi": 540.1,
"dSigmaHPsi": 324.0,
"asymmetryPct": -8.7,
"nearestParentId": "P2",
"nearestParentFt": 660,
"bashingRisk": "high"
}
],
"summary": {
"highCount": 1,
"watchCount": 1,
"lowCount": 0,
"worstStageId": "Stage 2",
"worstDepletionPsi": 540.1,
"meanDepletionPsi": 426.25,
"meanDSigmaHPsi": 255.8
}
}Team-scoped simulation rows. Same shape used by the in-app Cloud simulations page. Status must be one of not_yet_submitted, queued, running, completed, failed, cancelled.
# List
curl https://wellboregenius.com/api/public/sdk/v1/simulations \
-H "Authorization: Bearer dh_live_..."
# Create
curl -X POST https://wellboregenius.com/api/public/sdk/v1/simulations \
-H "Authorization: Bearer dh_live_..." \
-H "Content-Type: application/json" \
-d '{"workspaceId":"default","name":"bakken_baseline","tags":["history-match"]}'
# Retrieve (includes results JSON)
curl https://wellboregenius.com/api/public/sdk/v1/simulations/<id> \
-H "Authorization: Bearer dh_live_..."
# Update status / attach results
curl -X PATCH https://wellboregenius.com/api/public/sdk/v1/simulations/<id> \
-H "Authorization: Bearer dh_live_..." \
-H "Content-Type: application/json" \
-d '{"status":"completed","runtimeSeconds":1843,"results":{"peakBhpPsi":9120}}'
# Delete
curl -X DELETE https://wellboregenius.com/api/public/sdk/v1/simulations/<id> \
-H "Authorization: Bearer dh_live_..."- POST
/sensitivity/run— OFAT factor sweeps - Published
pip install downholewith typed dataclasses