Develop
API reference
Every endpoint, every parameter, and every response shape — typed and versioned.
What's live
Base URL
https://www.nodedata.ai/api/v1All endpoints accept JSON bodies and return JSON responses. Errors follow the same shape across the API.
Use the www host
nodedata.ai domain redirects to www.nodedata.ai, and fetch strips the Authorization header across that cross-origin redirect — the symptom is missing_credentials even though you sent a key. Call the www host directly.Versioning
Breaking changes ship under a new path prefix (/v1 → /v2). Additive changes (new fields, new optional parameters) ship in place without bumping the version. Removed or renamed fields are announced in the changelog at least 90 days before removal.
Error format
{
"error": {
"code": "price_too_low",
"message": "Minimum paid price is $1.00.",
"request_id": "req_9f2c1ab74e05d3819c6a7b40"
}
}code is stable — branch on it rather than parsing messages. Every error response also repeats its request_id in an x-request-id header. Log it: quoting it in a support request turns “a call failed yesterday” into one log line.
Pagination
List endpoints return a cursor in next_cursor when more results exist. Pass it back as ?cursor= on the next call; the cursor is the id of the last item on the page.
{
"items": [/* ... */],
"next_cursor": "cm5x9q2ab0001abcdefghij",
"has_more": true
}Cursors beat ?page= on a collection that changes underneath you: publishing one listing between calls would shift every offset and make you skip or repeat a record.
Rate limits
There are no per-key rate limits today, and the API does not return X-RateLimit-* headers — don't build a client that depends on reading them. Be a good citizen anyway: retry 429 and 5xx with exponential backoff and jitter, honour Retry-After if it appears, and avoid unbounded concurrency against list endpoints. Limits will be introduced with notice in the changelog.
Account
Get the authenticated account
GET /v1/me
Authorization: Bearer <key>
Response — 200
{
"id": "cm5x9q2ab0000abcdefghij",
"slug": "casper-white-2",
"name": "Casper White",
"email": "team@example.com",
"created_at": "2026-06-24T18:03:11.000Z",
"key": { "mode": "live", "scopes": ["models:read"] }
}Smallest possible call to verify a key. Returns the account that owns the key plus the key's mode and scopes. An empty scopes array means the key is unrestricted.
Inference usage
GET /v1/usage?days=30
Authorization: Bearer <key>
Response — 200
{
"window_days": 30,
"summary": { "calls": 1841, "total_tokens": 4192883, "cost_micros": 2891422, "cost_usd": 2.891422 },
"by_model": [{ "model": "node-reason-70b", "calls": 412, "total_tokens": 2222232, "cost_usd": 2.413 }]
}Models
List models
GET /v1/models
Authorization: Bearer <key>
Query parameters
q string Full-text search over title, description, and tags
type string Asset type — dataset | onnx-model | rl-policy | ros-package | …
category string Category slug — manipulation | navigation | vision | slam | …
cursor string Pagination cursor from the previous response's next_cursor
limit int 1–100 (default 20)Requires models:read. Returns published listings that have passed validation.
Model object
{
"id": "cm5x9q2ab0001abcdefghij",
"slug": "bin-picking-policy-v2",
"title": "Bin-Picking Policy v2",
"description": "Diffusion policy trained on 40k bimanual grasps.",
"type": "rl-policy",
"category": "manipulation",
"license": "Apache-2.0",
"version": "2.0.0",
"price": { "amount": 4900, "currency": "usd" },
"tags": ["grasping", "bimanual"],
"frameworks": ["pytorch", "ros2"],
"hardware": ["jetson-orin"],
"sensors": ["realsense-d435"],
"ros_compatible": true,
"jetson_compatible": true,
"isaac_sim_compatible": false,
"file": { "name": "policy.zip", "size": 184320042, "extension": "zip" },
"downloads": 312,
"featured": false,
"author": { "slug": "casper-white-2", "name": "Casper White" },
"created_at": "2026-07-02T09:14:00.000Z",
"updated_at": "2026-07-30T11:00:00.000Z"
}price.amount is integer cents — 4900 is $49.00. Ids are opaque strings; don't parse them. Timestamps are ISO 8601 UTC.
Retrieve a model
GET /v1/models/{id_or_slug}Accepts either the id or the URL slug. Unpublished and failed-validation listings are not addressable.
Revisions are not addressable
version string and previous versions are not retained, so there is no /revisions/{revision} endpoint. To keep versions separately downloadable today, publish each as its own listing — see model versioning.Publish a model
POST /v1/models
Authorization: Bearer <key>
{
"title": "Bin-Picking Policy v2",
"description": "Diffusion policy trained on 40k bimanual grasps.",
"type": "rl-policy",
"category": "manipulation",
"license": "Apache-2.0",
"version": "2.0.0",
"price_cents": 4900,
"frameworks": ["pytorch", "ros2"],
"storage_path": "user/<your-id>/1754390000-policy.zip",
"file_name": "policy.zip",
"file_size": 184320042,
"file_extension": "zip"
}
Response — 201 Created (the model object)Requires models:upload. price_cents is 0 for free or at least 100 for paid; anything between 1 and 99 is rejected with price_too_low. The slug is derived from the title and de-duplicated for you.
Update or unpublish
PATCH /v1/models/{id_or_slug} { "price_cents": 3900, "version": "2.0.1" }
DELETE /v1/models/{id_or_slug} # unpublish — purchase history is preservedBoth require listings:write. Another account's listing reads as 404, not 403, so the API can't be used to probe what exists.
Download a model
POST /v1/models/{id_or_slug}/download
Authorization: Bearer <key>
Response — 200
{
"url": "https://<storage>/object/sign/assets/...?token=...",
"expires_in": 120,
"file_name": "weights.safetensors",
"file_size": 184320042
}Returns a signed URL rather than redirecting, so a client can hand the transfer to whatever downloader it likes. Needs models:read or datasets:read. Paid assets you haven't bought return 402 purchase_required with the price attached.
Links are short-lived
Upload a file
POST /v1/uploads
Authorization: Bearer <key>
{ "file_name": "policy.onnx", "file_size": 4194304 }
Response — 200
{
"storage_path": "user/<your-id>/1754390000-policy.onnx",
"upload_url": "https://<storage>/object/upload/sign/assets/...?token=...",
"token": "...",
"file_name": "policy.onnx",
"file_size": 4194304,
"file_extension": ".onnx"
}
PUT <upload_url> # the bytes go straight to storage
PUT /v1/uploads # optional: { "storage_path": ... } → { exists, size }
GET /v1/uploads # limits: { max_bytes, extensions }Then pass storage_path, file_name, file_size and file_extension to POST /v1/models. Requires models:upload or datasets:upload. The create call re-checks that the path is yours and that the object exists, so a signed URL on its own publishes nothing.
Datasets
Datasets mirror models across the data-bearing types (dataset, telemetry, calibration) and are gated behind the dataset scopes, so a key can be granted data access without model access.
GET /v1/datasets— list, same parameters as modelsPOST /v1/datasets— create (type defaults todataset)GET /v1/datasets/{id_or_slug}PATCH /v1/datasets/{id_or_slug}DELETE /v1/datasets/{id_or_slug}— unpublishPOST /v1/datasets/{id_or_slug}/publish
Shards
A large dataset uploads as many objects under one prefix rather than a single file. That keeps each transfer inside the per-object size limit and makes an interrupted upload restartable at shard granularity — you re-upload what's missing, not the whole set.
# 1. Create the dataset (publish it once the data is complete)
POST /v1/datasets
{ "title": "Warehouse Depth 40k", "description": "…", "price_cents": 0 }
# 2. Mint an upload URL per shard, then PUT the bytes straight to storage
POST /v1/datasets/{id}/shards
{ "name": "shard-0001.npz", "size": 41943040 }
→ { "name": "shard-0001.npz", "storage_path": "…", "upload_url": "…", "token": "…" }
# 3. Check what actually landed — read from storage, not a database guess
GET /v1/datasets/{id}/shards
→ { "items": [{ "name": "shard-0001.npz", "size": 41943040, … }], "count": 1, "total_bytes": 41943040 }
# 4. Fetch one shard back
GET /v1/datasets/{id}/shards/{name}
→ { "name": "…", "size": …, "url": "…", "expires_in": 120 }
# 5. Publish once the set is complete
POST /v1/datasets/{id}/publishRestartable, not byte-resumable
Listings
The marketplace-facing view of the same rows, addressed by what a seller manages. The split exists so listings:write can let a key reprice and publish what already exists without also being able to upload new artifacts.
GET /v1/listings— every published listing, any typePOST /v1/listings— create a storefront entry for an assetGET /v1/listings/{id_or_slug}PATCH /v1/listings/{id_or_slug}— pricing, title, metadataPOST /v1/listings/{id_or_slug}/publish— idempotentPOST /v1/listings/{id_or_slug}/unpublish— idempotent
Purchases
POST /v1/purchases
Authorization: Bearer <key>
Content-Type: application/json
{ "asset": "bin-picking-policy-v2" }
Response — 201 Created
{
"object": "checkout_session",
"id": "cs_test_a1b2...",
"status": "requires_payment",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_a1b2...",
"expires_at": 1785934800,
"amount_cents": 4900,
"currency": "usd",
"asset": { "id": "cm5x9...", "slug": "bin-picking-policy-v2", "title": "Bin-Picking Policy v2" }
}Node Data is not PCI-scoped and never touches card data, so a keyed integration can't submit a payment method directly. Creating a purchase returns a Stripe Checkout session for the buyer to complete; the Purchase row is written by the Stripe webhook at fulfilment — the same path the website uses, so an API caller and the site can never disagree about what a completed sale is. Pass success_url and cancel_url to return the buyer to your own app.
GET /v1/purchases lists what this account has bought — check it before prompting someone to buy something they already own.
Payouts & balance
GET /v1/payouts answers “what have I sold and what am I owed”. GET /v1/balance_transactions is the ledger behind that number — every individual movement, so your books reconcile line by line. Both require payouts:read.
GET /v1/balance_transactions
Authorization: Bearer <key>
Query parameters
type string sale | refund | payout
limit int 1–200 (default 50)
Response — 200
{
"items": [
{
"id": "bt_sale_cm5x9...",
"type": "sale",
"amount_cents": 4900,
"fee_cents": 1225,
"net_cents": 3675,
"currency": "usd",
"status": "pending",
"created_at": "2026-08-01T22:10:00.000Z",
"source": { "purchase_id": "cm5x9...", "asset": { "slug": "…", "title": "…" } },
"description": "Sale of Bin-Picking Policy v2"
}
],
"summary": {
"currency": "usd",
"gross_cents": 129400,
"platform_fee_cents": 32350,
"earned_cents": 97050,
"refunded_cents": 0,
"paid_out_cents": 0,
"balance_cents": 97050,
"sale_count": 26,
"refund_count": 0
},
"payouts_paused": false
}Entries are derived from sales rather than stored separately, so they can't drift from the purchases they describe. A completed sale is a sale credit, a refund is a matching debit, and a settled creator transfer is a payout debit. Sum net_cents and you get the accruing balance.
AI Factory
Describe what you want and Factory drafts it for review. Requires the factory:run scope, and each generation spends a Factory credit — the same accounting as the web flow, so scripts can't run the pipeline for free.
POST /v1/factory/jobs
Authorization: Bearer <key>
{ "prompt": "Synthetic defect images for manufacturing QC." }
# Poll the pipeline
GET /v1/factory/jobs/{id}
# queued → analyzing → awaiting_clarification → planning →
# awaiting_approval → generating → review → published
POST /v1/factory/jobs/{id}/clarify { "answers": [{ "id": "…", "answer": "…" }] }
POST /v1/factory/jobs/{id}/approve
POST /v1/factory/jobs/{id}/regenerate?stage=plan|build
POST /v1/factory/jobs/{id}/publish { "price_cents": 4900 }See AI Factory for the full pipeline.
Inference
OpenAI-compatible completions, so any OpenAI client pointed at this base URL works. Requires a premium key with inference:run.
GET /v1/inference/models # catalog with live per-token pricing
POST /v1/chat/completions # set "stream": true for SSE chunksWebhooks
GET /v1/webhook_endpointsPOST /v1/webhook_endpoints— returns the signing secret onceGET /v1/webhook_endpoints/{id}PATCH /v1/webhook_endpoints/{id}DELETE /v1/webhook_endpoints/{id}GET /v1/webhook_endpoints/{id}/events— delivery log
See Authentication for signature verification.
Deployments
Not available
/v1/deployments resource and no deployment host. This section previously documented that interface as though it existed.What works today: download the artifact with POST /v1/models/{id}/download and run it on your own hardware, or use POST /v1/chat/completions for hosted text inference on the Node model catalog. See deploying models for the surfaces that are real.