Hosted HTTP SDKs
The Hosted HTTP SDKs call a prepared xplo workbook from Go, TypeScript, Python, or Rust. They wrap the Runtime API, use the programmatic names configured in Naming + Labelling, and accept canonical cell addresses directly. Naming is optional: every SDK can run a workbook that has no labels at all.
Two families, one product. These SDKs run the calculation on xplo's servers: every call is a network request with a token, and every run is metered. The other family — the Local Runtime Clients (Python, TypeScript, Go, Java) — runs the model in your own process on a downloaded bundle, with no network and no metering. Neither silently falls back to the other. Pick by where the calculation runs.
Hosted HTTP SDKs versus Local Runtime Clients
| Hosted HTTP SDK (this page) | Local Runtime Client | |
|---|---|---|
| Where the calculation runs | xplo-managed Runtime API | Your process |
| Needs network + token | Yes, every run | No, after the download |
| Metered per run | Yes | No |
| Model artifact | Stays on our servers | A bundle you download |
| Main type | XploHttpClient (xplohttp.Client in Go) |
Model / loadBundle |
| Languages | Go, TypeScript, Python, Rust | Python, TypeScript, Go, Java |
The two lists are not the same: there is no Java Hosted HTTP SDK (call the
Runtime API directly from the JVM — fetch the cell
interface, POST a run), and there is no Rust Local Runtime Client. Every Hosted HTTP SDK
name contains http; a Local Runtime Client name never does. An HTTP SDK never
downloads or executes model code, starts vmrun, or falls back to local
execution; a Local Runtime Client never sends workbook inputs to xplo.
Install
# Go
go get github.com/Pythia-Software/explo/clients/http/go
# TypeScript (Yarn)
yarn add @xplo/http-client
# Python
python -m pip install xplo-http-client
# Rust
cargo add xplo-http-client
Not published yet. None of these packages is on a public registry (the Go module proxy, npm, PyPI, crates.io), so the commands above do not resolve today. The coordinates are what the commands will be once released. Until then, use one of the two paths below.
1. Call the Runtime API directly. These SDKs are thin: an authenticated
POST /actions/{id}/run, a GET /actions/{id}/cell-interface to pin the
revision, and typed result structs. Anything curl or your language's HTTP
client can do, they do — the Runtime API guide has the
complete wire contract, including the semantic envelope these SDKs send. This is
the only path that needs nothing from us at all, and it is the answer for Java
and every other language.
2. Build from a checkout of the xplo repository, if you have one. The
sources live under clients/http/. Every path below is relative to the
consuming project, so use an absolute path into your checkout — a bare
./clients/http/... resolves inside your project and will not exist:
# Python
pip install /path/to/explo/clients/http/python
# TypeScript — build first (dist/ is generated, not committed), then add by path
(cd /path/to/explo/clients/http/typescript && yarn install && yarn build)
yarn add file:/path/to/explo/clients/http/typescript
# Go — from your module root
go mod edit -replace github.com/Pythia-Software/explo/clients/http/go=/path/to/explo/clients/http/go
go mod tidy
# Rust — in Cargo.toml (a path relative to this Cargo.toml, or absolute)
# xplo-http-client = { path = "/path/to/explo/clients/http/rust" }
Note the asymmetry with the Local Runtime Clients: those can be downloaded as a
source zip from GET /api/v2/clients/{lang} and are vendored inside every model
bundle. The Hosted HTTP SDKs are not — that endpoint serves the local
runtime family only.
All four SDKs are versioned independently from downloaded workbook bundles and
from the Local Runtime Clients. The TypeScript package is ESM-only — the
consuming file must be .mjs or the project package.json must set
"type": "module".
The model handle and named interface
Every SDK follows the same lifecycle. Create or manage the credential for step 1 in API Tokens.
- Construct an HTTP client with an
xplo_API token. - Load a model handle for a completed action ID. This fetches the current cell interface, including its programmatic keys, labels, units, aliases, and revision.
- Run the model with named inputs and outputs. The request includes the loaded interface revision.
- If someone changes the interface between steps 2 and 3, the server returns
409 CONFLICT. Refresh the handle, inspect the new contract, and rebuild the request deliberately.
This prevents a key from silently changing meaning during a deployment. The server resolves each key to its canonical workbook-qualified cell inside the same immutable revision used for the run. Renamed aliases continue to work when the interface marks them as resolvable, and the response includes a deprecation warning and canonical key.
Omitting the output argument means "all current named outputs." Passing an
explicit empty list means "no named outputs." Inputs are currently numeric;
booleans use 1/0, and dates use Excel serial values.
An action with no confirmed labels has no named interface. A named run
against it succeeds with an empty result ({} named outputs, no warning) and
still consumes a metered run — it is not an error. Check that the handle's
interface has labels before running, or use the raw address path below and
GET /actions/{id}/vm/manifest to discover legal addresses. The SDKs do not
wrap that manifest endpoint; fetch it over plain HTTP.
refresh() does not mutate the handle in any of the four SDKs — it returns a
new one at the current revision. Always reassign:
model = model.refresh(). Dropping the result leaves you pinned to the stale
revision and the next run 409s again.
Python
import os
from xplo_http import XploHttpClient, XploHttpError
client = XploHttpClient(
token=os.environ["XPLO_TOKEN"],
# Optional; a host-only URL also works and receives /api/v2 automatically.
base_url="https://xplo.pythia.software/api/v2",
)
model = client.model("act-...")
inputs = {"loan_amount": 325_000, "interest_rate": 0.06}
try:
result = model.run(inputs, ["monthly_payment"])
except XploHttpError as error:
if error.status != 409:
raise
# Labels changed under us: refresh the handle (it returns a new one), review
# the new interface, then rebuild and rerun deliberately.
model = model.refresh()
result = model.run(inputs, ["monthly_payment"])
print(result.named["monthly_payment"].value)
Canonical address escape hatch:
raw = model.run_raw(
{"Loan!B1": 325_000, "Loan!B2": 0.06},
["Loan!B8"],
)
print(raw.outputs["Loan!B8"].value)
TypeScript
import { XploHttpClient, XploHttpError } from '@xplo/http-client';
const client = new XploHttpClient({
token: process.env.XPLO_TOKEN!,
baseUrl: 'https://xplo.pythia.software/api/v2',
});
let model = await client.model('act-...');
const inputs = { loan_amount: 325_000, interest_rate: 0.06 };
let result;
try {
result = await model.run(inputs, ['monthly_payment']);
} catch (error) {
if (!(error instanceof XploHttpError) || error.status !== 409) throw error;
// Labels changed under us: refresh the handle (it returns a new one), review
// the new interface, then rebuild and rerun deliberately.
model = await model.refresh();
result = await model.run(inputs, ['monthly_payment']);
}
console.log(result.outputs.named.monthly_payment.value);
Canonical address escape hatch:
const raw = await model.runRaw(
{ 'Loan!B1': 325_000, 'Loan!B2': 0.06 },
['Loan!B8'],
);
console.log(raw.outputs['Loan!B8'].value);
The TypeScript package uses the host's WHATWG fetch; Node 18 and later
provide it natively.
Go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
xplohttp "github.com/Pythia-Software/explo/clients/http/go"
)
func main() {
ctx := context.Background()
client, err := xplohttp.NewClient(
os.Getenv("XPLO_TOKEN"),
xplohttp.WithBaseURL("https://xplo.pythia.software/api/v2"),
xplohttp.WithHTTPClient(&http.Client{Timeout: 20 * time.Second}),
)
if err != nil {
log.Fatal(err)
}
model, err := client.Model(ctx, "act-...")
if err != nil {
log.Fatal(err)
}
result, err := model.Run(ctx, xplohttp.NamedInputs{
"loan_amount": 325000,
"interest_rate": 0.06,
}, "monthly_payment")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Outputs.Named["monthly_payment"].Value)
}
WithBaseURL and WithHTTPClient are the two client options; the second is how
you set a deadline, since the SDK has no timeout knob of its own.
Canonical address escape hatch:
raw, err := model.RunRaw(ctx, xplohttp.RawInputs{
"Loan!B1": 325000,
"Loan!B2": 0.06,
}, "Loan!B8")
Run is a convenience over RunNamed, which carries the full selector surface
(below). Note the asymmetry: Run(ctx, inputs) with no output names means all
named outputs, so selecting none requires
RunNamed(ctx, xplohttp.NamedRunRequest{Outputs: &xplohttp.NamedOutputs{}}).
Use errors.As with a var apiError *xplohttp.HTTPError target to inspect
StatusCode, Code, and RetryAfter. Refresh returns a new handle, so
assign it back: model, err = model.Refresh(ctx).
Rust
The Rust SDK currently exposes a blocking client:
use std::collections::BTreeMap;
use xplo_http::{Error, XploHttpClient};
fn main() -> Result<(), Error> {
let client = XploHttpClient::new(std::env::var("XPLO_TOKEN").unwrap())?;
let model = client.model("act-...")?;
let result = model.run(
BTreeMap::from([
("loan_amount".to_owned(), 325_000.0),
("interest_rate".to_owned(), 0.06),
]),
Some(vec!["monthly_payment".to_owned()]),
)?;
println!("{}", result.outputs.named["monthly_payment"].value);
Ok(())
}
Canonical address escape hatch:
let raw = model.run_raw(
BTreeMap::from([
("Loan!B1".to_owned(), 325_000.0),
("Loan!B2".to_owned(), 0.06),
]),
Some(vec!["Loan!B8".to_owned()]),
)?;
Match Error::Api(error) to inspect status, code, and retry_after.
refresh() returns a new handle, so bind it back: let model = model.refresh()?;.
Result shapes differ by one level
The four SDKs send the same request and expose the same information, but Python
flattens the named-run response while the other three keep the wire's outputs
envelope. This is the one place a port from one language to another does not
transliterate:
| Language | Named outputs | Raw locator outputs | Address run |
|---|---|---|---|
| Python | result.named["k"].value |
result.cells |
raw.outputs["Loan!B8"].value |
| TypeScript | result.outputs.named.k.value |
result.outputs.cells |
raw.outputs['Loan!B8'].value |
| Go | result.Outputs.Named["k"].Value |
result.Outputs.Cells |
raw.Outputs["Loan!B8"].Value |
| Rust | result.outputs.named["k"].value |
result.outputs.cells |
raw.outputs["Loan!B8"].value |
Mixed named and raw selectors
All four SDKs let one run combine names and structured raw cell locators. Use
this when most application-facing values have stable names but a temporary or
diagnostic cell does not. A locator contains workbook_id, sheet, and A1
address; multi-workbook models require the workbook ID. The switch that
echoes every current named input back is in the same place.
Each SDK spells it differently, so here is the same run in all four:
from xplo_http import CellInput, CellLocator
result = model.run(
{"loan_amount": 325_000},
["monthly_payment"],
raw_inputs=[CellInput(cell=CellLocator(sheet="Loan", address="C7"), value=0.02)],
raw_outputs=[CellLocator(sheet="Loan", address="D9")],
all_named_inputs=True,
)
print(result.named["monthly_payment"].value, [c.value for c in result.cells])
const result = await model.run({ loan_amount: 325_000 }, ['monthly_payment'], {
rawInputs: [{ cell: { sheet: 'Loan', address: 'C7' }, value: 0.02 }],
rawOutputs: [{ sheet: 'Loan', address: 'D9' }],
allNamedInputs: true,
});
console.log(result.outputs.named.monthly_payment.value, result.outputs.cells);
outputs := xplohttp.NamedOutputs{"monthly_payment"}
result, err := model.RunNamed(ctx, xplohttp.NamedRunRequest{
Inputs: xplohttp.NamedInputs{"loan_amount": 325000},
Outputs: &outputs, // nil = all named outputs; &NamedOutputs{} = none
RawInputs: []xplohttp.CellInput{{
Cell: xplohttp.CellLocator{Sheet: "Loan", Address: "C7"},
Value: 0.02,
}},
RawOutputs: []xplohttp.CellLocator{{Sheet: "Loan", Address: "D9"}},
AllNamedInputs: true,
})
use xplo_http::{CellInput, CellLocator, NamedRunOptions};
let result = model.run_with_options(
BTreeMap::from([("loan_amount".to_owned(), 325_000.0)]),
NamedRunOptions {
outputs: Some(vec!["monthly_payment".to_owned()]),
raw_inputs: vec![CellInput {
cell: CellLocator {
workbook_id: None,
sheet: "Loan".to_owned(),
address: "C7".to_owned(),
},
value: 0.02,
}],
raw_outputs: vec![CellLocator {
workbook_id: None,
sheet: "Loan".to_owned(),
address: "D9".to_owned(),
}],
all_named_inputs: true,
},
)?;
Named and raw selectors that target the same input are rejected. Selected outputs may share a physical target — the runtime computes the cell once and projects it under each requested name/locator.
run_raw/RunRaw is not revision-pinned. It deliberately omits
expected_interface_revision, because it addresses physical cells rather than
names, so it never returns 409 for an interface change. The safety the model
handle buys you does not extend to it: if the workbook layout moves, a raw run
silently targets different cells. Prefer names for anything long-lived.
Errors, timeouts, and retries
The SDKs preserve the Runtime API's status, error code/message, and
Retry-After value. Their default client deadline is 35 seconds, slightly
longer than the Runtime API's current 30-second execution bound. Configure a
shorter deadline when your application requires one — in Go that means
WithHTTPClient(&http.Client{Timeout: …}); the other three take the deadline as
a client option.
409 has more than one cause, and the SDKs surface a single conflict code for
all of them: a stale expected_interface_revision, a model artifact that
changed while the request was being prepared, and a model that is not ready.
Only the first is fixed by refresh() + rerun. Because every attempt is a new
metered run, branch on the error message rather than auto-refreshing on any
409.
No SDK automatically retries POST /run. If your application chooses to retry a
transient 500 or 503, use bounded exponential backoff and make the
additional execution explicit in your usage accounting. Fix 400 and 409
responses before retrying an unchanged request.
See also
- Runtime API — the HTTP contract these SDKs wrap
- Local Runtime Clients: Python · TypeScript · Go · Java
- Naming + Labelling — optional aliases for cell addresses, not a setup requirement