nodedata — Python SDK
Official Python client for the Node Data API: search and download robotics/physical-AI models and datasets, publish your own, read your payouts, subscribe to webhooks, and run Node Data's paid inference.
Zero runtime dependencies. Standard library only, so it installs onto a Jetson next to whatever pinned torch/numpy stack is already there.
pip install nodedata
pip install -e ".[dev]" # from this directory, for development
Requires Python 3.9+.
Quick start
Create a key at /dashboard/api-keys
(nd_test_… or nd_live_…) and export it:
export NODE_DATA_API_KEY=nd_live_...
from nodedata import NodeData
nd = NodeData() # reads NODE_DATA_API_KEY
me = nd.me()
print(me.name, me.key.mode, me.key.scopes)
# Search the marketplace
for listing in nd.models.iter_all(q="grasp", type="dataset", max_items=50):
print(listing.slug, listing.price.dollars, listing.url)
# Download something you own or that's free
path = nd.models.download("acme-panda-grasp-100k", "./cache")
# Publish a build artifact — uploads the file, then creates the listing
listing = nd.models.publish(
"dist/grasp-policy-v2.onnx",
title="Grasp policy v2",
description="Trained on 100k teleop episodes.",
type="policy",
price_cents=4900, # 0 for free; paid minimum is 100
frameworks=["pytorch"],
)
print(listing.url)
What's covered
| Area | Calls |
|---|---|
| Account | nd.me() |
| Listings | nd.models.list() · iter_all() · retrieve() · create() · update() · unpublish() |
| Files | nd.models.download() · download_url() · nd.uploads.upload_file() · create() · confirm() · rules() |
| Publish | nd.models.publish() (upload + create in one call) |
| Payouts | nd.payouts.retrieve() |
| Usage | nd.usage.retrieve(days=30) |
| Webhooks | nd.webhooks.list() · create() · retrieve() · update() · delete() · deliveries() · event_types() |
| Inference | nd.inference.models() · chat() · stream() · stream_text() · ask() |
| Anything newer | nd.request("GET", "/some/new/endpoint") |
nd.datasets is an alias of nd.models — one endpoint serves both.
Every returned object keeps the raw response in .raw, so a field the API adds
after this release is still readable without upgrading.
Pagination
list() returns one Page; iter_all() walks every page lazily.
page = nd.models.list(limit=100)
print(len(page), page.has_more, page.next_cursor)
for listing in nd.models.iter_all(type="dataset"): # follows cursors
...
Inference
Requires a premium key with the inference:run scope. The endpoint is
OpenAI-compatible, so openai pointed at https://www.nodedata.ai/api/v1 works
too — these helpers just avoid the dependency.
for model in nd.inference.models():
print(model.id, model.context_window, model.pricing.input_per_1m)
print(nd.inference.ask("Summarise this dataset card.", model="node-reason-1"))
for text in nd.inference.stream_text(
model="node-reason-1",
messages=[{"role": "user", "content": "Explain grasp policies"}],
):
print(text, end="", flush=True)
The final streamed chunk carries usage and no text — that's the frame to log
spend against.
Webhooks
Verify with the raw request body. Re-serialising parsed JSON changes bytes, and the signature won't match.
from nodedata import verify_webhook, WebhookVerificationError
endpoint = nd.webhooks.create(
"https://example.com/hooks/nodedata",
events=["listing.purchased", "payout.paid"],
)
print(endpoint.secret) # returned on create only — store it now
# in your handler
try:
event = verify_webhook(request.body, request.headers["nd-signature"], SECRET)
except WebhookVerificationError:
return 400
verify_webhook enforces HMAC-SHA256 with a constant-time compare and a 300s
replay window, matching the server.
Errors
Everything raises a subclass of NodeDataError:
| Exception | Status | Typical cause |
|---|---|---|
InvalidRequestError |
400 | validation failed |
AuthenticationError |
401 | missing, revoked or expired key |
PaymentRequiredError |
402 | premium_required, purchase_required, unpaid key |
PermissionDeniedError |
403 | key lacks the scope |
NotFoundError |
404 | no such object — or not yours |
RateLimitError |
429 | carries retry_after |
MaintenanceError |
503 | platform deliberately closed; carries retry_after |
ServerError |
5xx | API failure |
APIConnectionError / APITimeoutError |
— | never reached the API |
Branch on exc.code (purchase_required, scope_required, price_too_low, …)
rather than the message text.
from nodedata import PaymentRequiredError
try:
nd.models.download("some-paid-asset", "./cache")
except PaymentRequiredError as exc:
if exc.code == "purchase_required":
print(f"costs ${exc.body['price_cents'] / 100:.2f}")
Retries
Rate limits and transient failures are retried twice by default with jittered
backoff, honouring Retry-After. POST is only retried on a 429 — the server
rejected it before doing any work — so a publish can never double-apply.
nd = NodeData(timeout=30, max_retries=5)
CLI
Installing the package also installs a nodedata command:
nodedata whoami
nodedata search grasp --type dataset
nodedata download acme-panda-grasp-100k -o ./cache
nodedata publish dist/policy.onnx --title "Grasp policy" --description "..." --type policy
nodedata payouts
nodedata usage --days 7
nodedata ask "Explain grasp policies" --model node-reason-1
Configuration
| Env var | Purpose |
|---|---|
NODE_DATA_API_KEY |
API key used when none is passed |
NODE_DATA_BASE_URL |
Override the API base URL (preview deploys, self-host) |
Certificates
Because this SDK uses urllib rather than bundling certifi, TLS trust comes
from the system store. Some Python builds — notably the macOS python.org
installers — ship an empty one, which fails every HTTPS request with
CERTIFICATE_VERIFY_FAILED. Three fixes, in order of ease:
pip install certifi # used automatically if present
open "/Applications/Python 3.x/Install Certificates.command"
import ssl
nd = NodeData(ssl_context=ssl.create_default_context(cafile="/path/to/ca.pem"))
The SDK detects this specific failure and says so, rather than reporting it as an API outage.
Tests
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest # hermetic; runs a local HTTP server
NODE_DATA_LIVE_KEY=nd_live_... .venv/bin/python -m pytest tests/test_live.py -v
The suite talks to a real socket rather than a patched transport, so retries, SSE framing and streamed downloads are exercised as they run in production. The live suite is read-only.
Versioning
Minor releases may add response fields. Pin in CI and bump deliberately.
MIT © Node, Inc.
Release files for nodedata 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nodedata-0.1.3.tar.gz | 38.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nodedata-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 73.1 kB
Release files / nodedata-0.1.3.tar.gz
| Download URL | nodedata-0.1.3.tar.gz |
|---|---|
| Size | 38.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b837949a535b846427157d3f284924f3eefd775e5b5dc5bf6362f35949731146
|
|
BLAKE2b-256 checksum How to use checksums |
81db59973028b4f3139d374e0328a6931a7de3956764da3bb2354a48e4aacadc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / nodedata-0.1.3-py3-none-any.whl
| Download URL | nodedata-0.1.3-py3-none-any.whl |
|---|---|
| Size | 34.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f7d5d133afb81428727d774bdad4224a9445d6215aa0c6c7b0e96ba5fcd2191c
|
|
BLAKE2b-256 checksum How to use checksums |
b82553414459d70b34505ea1071b54fc0dac256c3145a81dbe7540a2bc0fc180
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|