endpoints
Inference endpoints (M15) — hub-served inference for the skill / planner layer, never the reflex layer. Claimed by lucen serve, consumed by a device only through a signed approval bound to device + model digest + endpoint id, metered per session.
GET /v1/endpoints
List inference endpoints
Endpoints the caller may see: their own and those billed to an org they belong to. No endpoint is public. Newest first; an unknown owner or model_repo is an empty page, not a 404. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | EndpointStatus | |
executor | query | RunExecutor | |
model_repo | query | string | Only endpoints serving this |
owner | query | Handle | |
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
| Status | Description | Body |
|---|---|---|
200 | Page of endpoints. | EndpointPage |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
422 | Request failed validation. | Problem |
POST /v1/endpoints
Create an inference endpoint for a skill-layer model
A model repo becomes a hub-served inference endpoint (M15). Invariant 1 of docs/ARCHITECTURE.md "Inference placement" is enforced here: the repo's ModelMeta.control_class must be skill or planner; a reflex model (a 50 Hz locomotion controller) answers 409 — nothing reflex-rate is ever served over a network by this hub — and a model that has declared no class answers 422 asking for one. The endpoint is pinned to one ONNX (onnx_path, or the repo's single root *.onnx) and its current sha256, exactly like a device approval request; the sha256 is what every session's approval token is later bound to. M17e — or to a checkpoint directory: a model with no ONNX export (the VLA smolvla_lora trains) is published as policy/ + lucen_manifest.json, and the endpoint is pinned to that directory (policy_path, or auto-detected when the repo holds no root ONNX) and to its lucen-checkpoint-v1 digest (EndpointArtifact); its control_class, contract fingerprint and params are read from the manifest, which the digest covers, and a manifest that says reflex is 409 whatever ModelMeta says. The hardware tier is chosen by the model's size, never by the caller: params × 2 bytes (bf16) plus 20 % headroom → the smallest tier that fits (a ≤ 3 B model lands on l4); params comes from the repo's io_contract.json / manifest, else from the body, else 422. executor: worker (the default) records the endpoint queued until lucen serve on the owner's own GPU box claims it — unpriced; executor: modal (W13) records the endpoint queued and the hub spawns the deployed endpoint function on the tier's GPU, which claims it with a public wss:// URL — on a deployment without MODAL_APP_NAME it answers 503 naming it and records nothing (never a fake running). key:train: an endpoint spends GPU time. Audited as endpoint.create.
Hosted execution (L0). executor: modal serves only owners in the deployment's HOSTED_OWNERS (default lucen; * = everyone); anyone else is 403 with errors[].type: forbidden.hosted_owner, naming the setting and lucen serve, and nothing is recorded. The estimate answers the same 403.
Request body
| Field | Type | Description |
|---|---|---|
model_repo | string |
|
onnx_path | FilePath | null | Which file in the repo the endpoint serves. Omit when the repo holds exactly one |
policy_path | FilePath | null | M17e. The checkpoint directory the endpoint serves, for a model with no ONNX export (a VLA): LeRobot's |
executor | RunExecutor | null |
|
owner | Handle | null | Who the endpoint is billed to (user or org). Omit for yourself. |
name | string | null | A label for the endpoint page. |
params | integer | null | The model's parameter count, used only when neither the repo's |
max_hours | number | null | A wall-clock cap the server enforces on itself; null means no cap. |
budget_usd | number | null | A hard cap on the endpoint's total cost (priced executors only). |
Responses
| Status | Description | Body |
|---|---|---|
201 | Endpoint recorded ( | Endpoint |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
503 | A service this endpoint needs is not configured in this deployment (M0's boot guarantee: the API boots with zero secrets, and an endpoint that needs one says so instead of returning a stack trace). | Problem |
POST /v1/endpoints/estimate
Price an endpoint before creating it
The same body as createEndpoint plus hours, validated identically (visibility, kind, control class, size), creating nothing. Answers the tier the model's size selects and rate_usd_per_hour × hours. A worker endpoint is cost_usd_max: 0, priced: false — the hub bills nothing for the owner's own GPU, and zero means "not charged", not "free GPU time" (M7-RL's rule). A modal estimate is priced even though that backend is not built: the price is true before "and it cannot run here yet" is. (M15)
Hosted execution (L0). executor: modal serves only owners in the deployment's HOSTED_OWNERS (default lucen; * = everyone); anyone else is 403 with errors[].type: forbidden.hosted_owner, naming the setting and lucen serve, and nothing is recorded. The estimate answers the same 403.
Request body
| Field | Type | Description |
|---|---|---|
model_repo | string | |
onnx_path | FilePath | null | |
policy_path | FilePath | null | M17e. As on |
executor | RunExecutor | null | |
owner | Handle | null | |
params | integer | null | |
hours | number | How many hours to price; the endpoint itself has no fixed duration. |
budget_usd | number | null |
Responses
| Status | Description | Body |
|---|---|---|
200 | The estimate. | EndpointEstimate |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/endpoints/{endpoint_id}
Get an inference endpoint
One endpoint with its live counters (open sessions, chunks served, GPU-seconds, service and round-trip p95). An endpoint the caller may not see is 404, never 403. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
Responses
DELETE /v1/endpoints/{endpoint_id}
Stop an inference endpoint
A queued endpoint is stopped at once (nothing ran, nothing to bill). A running one has a server on the owner's own machine, so the hub sets stop_requested_at and answers 202 with the endpoint still running; lucen serve sees the flag on its next report, closes every session and reports stopped, which is when the sessions' ledger rows are written. Idempotent; a terminal endpoint is 409. key:train, like cancelRun. Audited as endpoint.stop. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
Responses
| Status | Description | Body |
|---|---|---|
202 | Stopped, or stop requested; the endpoint as it now stands. | Endpoint |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
POST /v1/endpoints/{endpoint_id}/claim
Claim a queued worker endpoint (`lucen serve` protocol)
The run protocol, mirrored: compare-and-set queued → running on an endpoint whose executor is worker. Exactly one caller wins; an endpoint already claimed, terminal, or not a worker endpoint answers 409. The server states the websocket URL devices will connect to and the sha256 of the file it loaded, which must equal the endpoint's model_sha256 (422 otherwise — a server serving other bytes than the ones approvals are bound to is refused before any device can reach it). Records claimed_by, the claiming credential (only it may reportEndpoint afterwards — 409 for anyone else), claimed_at, started_at and the first heartbeat_at; audited as endpoint.claim. key:train: the server spends the owner's GPU on the owner's behalf. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
Request body
| Field | Type | Description |
|---|---|---|
worker | string | A name for the server — its hostname, or anything the operator will recognise. |
url | string | The websocket URL devices connect to ( |
model_sha256 | Sha256 |
Responses
| Status | Description | Body |
|---|---|---|
200 | Claimed; the endpoint as it now stands. | Endpoint |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
POST /v1/endpoints/{endpoint_id}/report
Report sessions, metering and state for a claimed endpoint
The server's one write path after claimEndpoint, every few seconds. The endpoint must be running (409 otherwise) and the caller must be the credential that claimed it (409). sessions[] carries the server's numbers per session — chunks served, GPU-seconds attributed as wall time × 1 / concurrent open sessions (a shared inference GPU is split evenly across the sessions it served, and the row says so), service-time p50/p95 — and state: closed when the device's link went away. status: stopped | failed is terminal: every still-open session is closed, each session's ledger row is written (priced by the model's tier on modal, 0 / unpriced on worker), cost_usd is summed onto the endpoint and endpoint.finish is audited. Read stop_requested_at on the returned endpoint: when set, close every session and report stopped. Every report refreshes heartbeat_at. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
Request body
| Field | Type | Description |
|---|---|---|
status | EndpointReportStatus | null | |
sessions | array of EndpointSessionState | |
error | string | null | |
log_chunk | string | null | The server's log since the last report; kept on the endpoint's last-report record only. |
Responses
| Status | Description | Body |
|---|---|---|
200 | Recorded; the endpoint as it now stands. | Endpoint |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/endpoints/{endpoint_id}/sessions
List an endpoint's sessions (metering)
Newest first. Each session carries the server's metering (chunks served, GPU-seconds, service p50/p95), the device's own metering (chunks applied, round-trip p50/p95, deadline misses, degraded count) and, once closed, its cost. status narrows to open or closed. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
status | query | EndpointSessionStatus | |
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
| Status | Description | Body |
|---|---|---|
200 | Page of sessions. | EndpointSessionPage |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
422 | Request failed validation. | Problem |
POST /v1/endpoints/{endpoint_id}/sessions
Open a session on an endpoint (a device, with a human's approval)
Invariant 2: an endpoint is consumed only through the consent layer. The caller is the robot-side driver (lucen-device, a write key that can see the device) and it must present the EdDSA approval token a human minted with createApproval on a request that named this endpoint (ApprovalRequestCreate.endpoint_id). The hub verifies the token against its own signing key and answers 403 when it is missing, unsigned, expired, bound to another endpoint, bound to another device, or when the approval is no longer issued / running — the same word for every refusal, so a token cannot be probed. It then checks the token's onnx_sha256 against the endpoint's model_sha256 (409 when the endpoint was re-created on other bytes) and that a server has claimed the endpoint (409 while queued). On success it records the session and returns the server's websocket url plus a short-lived session token (EdDSA, typ: lucen-session+jwt, exp = min(approval exp, now + ENDPOINT_SESSION_TTL_S)) the device presents to the server as openpi's api_key header; the server verifies it offline against GET /v1/devices/signing-key. No hub API key can open a session without a signed approval, which is the whole point. Audited as endpoint.session_open. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
Request body
| Field | Type | Description |
|---|---|---|
device_id | Id | |
approval_token | string | null | The EdDSA approval token a human minted for this device on a request naming this endpoint. Missing or invalid is |
Responses
| Status | Description | Body |
|---|---|---|
201 | Session opened; connect to | EndpointSessionOpened |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
POST /v1/endpoints/{endpoint_id}/sessions/{session_id}/report
The device's own metering for a session
Only the device can measure a round trip, so it reports its side: chunks applied, round-trip p50/p95 in milliseconds, how many chunk deadlines were missed and how many times the local fail-safe fired (degraded_count). closed: true ends the session from the device's side (closed_reason says why: expired, local_cap, stopped, link_lost). The caller must be a credential that can see the session's device (404 otherwise); a closed session is 409. Each report replaces the device-side numbers — they are percentiles over the whole session, not deltas. key:write, like the driver's telemetry events. (M15)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint_id | path | Id | Endpoint id ( |
session_id | path | Id | Endpoint session id ( |
Request body
| Field | Type | Description |
|---|---|---|
chunks_applied | integer | null | |
rtt_ms_p50 | number | null | |
rtt_ms_p95 | number | null | |
deadline_misses | integer | null | |
degraded_count | integer | null | |
closed | boolean | |
closed_reason | string | null |
Responses
| Status | Description | Body |
|---|---|---|
200 | Recorded; the session as it now stands. | EndpointSession |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
Schemas (14)
The schemas these operations reach before any other tag’s do. A type that links elsewhere is rendered on that tag’s page.
EndpointPage
| Field | Type | Description |
|---|---|---|
items | array of Endpoint | |
next_cursor | string | null |
EndpointCreate
| Field | Type | Description |
|---|---|---|
model_repo | string |
|
onnx_path | FilePath | null | Which file in the repo the endpoint serves. Omit when the repo holds exactly one |
policy_path | FilePath | null | M17e. The checkpoint directory the endpoint serves, for a model with no ONNX export (a VLA): LeRobot's |
executor | RunExecutor | null |
|
owner | Handle | null | Who the endpoint is billed to (user or org). Omit for yourself. |
name | string | null | A label for the endpoint page. |
params | integer | null | The model's parameter count, used only when neither the repo's |
max_hours | number | null | A wall-clock cap the server enforces on itself; null means no cap. |
budget_usd | number | null | A hard cap on the endpoint's total cost (priced executors only). |
EndpointEstimateRequest
| Field | Type | Description |
|---|---|---|
model_repo | string | |
onnx_path | FilePath | null | |
policy_path | FilePath | null | M17e. As on |
executor | RunExecutor | null | |
owner | Handle | null | |
params | integer | null | |
hours | number | How many hours to price; the endpoint itself has no fixed duration. |
budget_usd | number | null |
EndpointEstimate
What an endpoint would cost per hour, on the tier its model's size selects.
| Field | Type | Description |
|---|---|---|
executor | RunExecutor | |
tier | string | Selected from the model's size: |
params | integer | The parameter count the tier was chosen from. |
vram_bytes | integer |
|
rate_usd_per_hour | number | Billed hourly rate for the tier (4 decimal places); 0 for a |
hours | number | |
cost_usd_max | number |
|
budget_usd | number | null | |
capped_by_budget | boolean | |
priced | boolean | False when the hub does not bill the executor ( |
artifact | EndpointArtifact | |
notes | array of string | M17e. What |
EndpointClaim
| Field | Type | Description |
|---|---|---|
worker | string | A name for the server — its hostname, or anything the operator will recognise. |
url | string | The websocket URL devices connect to ( |
model_sha256 | Sha256 |
EndpointReport
One progress or terminal report from the server. Every field is optional.
| Field | Type | Description |
|---|---|---|
status | EndpointReportStatus | null | |
sessions | array of EndpointSessionState | |
error | string | null | |
log_chunk | string | null | The server's log since the last report; kept on the endpoint's last-report record only. |
EndpointSessionStatus
EndpointSessionPage
| Field | Type | Description |
|---|---|---|
items | array of EndpointSession | |
next_cursor | string | null |
EndpointSessionOpen
| Field | Type | Description |
|---|---|---|
device_id | Id | |
approval_token | string | null | The EdDSA approval token a human minted for this device on a request naming this endpoint. Missing or invalid is |
EndpointSessionOpened
| Field | Type | Description |
|---|---|---|
session | EndpointSession | |
url | string | The server's websocket URL. |
session_token | string | EdDSA JWT ( |
expires_at | string (date-time) | |
model_sha256 | Sha256 | |
contract_fingerprint | ContractFingerprint | null |
EndpointSessionReport
The device's side of a session's metering; each report replaces the last.
| Field | Type | Description |
|---|---|---|
chunks_applied | integer | null | |
rtt_ms_p50 | number | null | |
rtt_ms_p95 | number | null | |
deadline_misses | integer | null | |
degraded_count | integer | null | |
closed | boolean | |
closed_reason | string | null |
EndpointSession
| Field | Type | Description |
|---|---|---|
id | Id | |
endpoint_id | Id | |
device_id | Id | null | |
approval_id | Id | null | |
status | EndpointSessionStatus | |
opened_at | string (date-time) | |
expires_at | string (date-time) | null (date-time) | When the session token dies — the approval's expiry or the hub's session TTL, whichever is sooner. |
closed_at | string (date-time) | null (date-time) | |
closed_reason | string | null | |
last_report_at | string (date-time) | null (date-time) | |
chunks_served | integer | As the server reported. |
gpu_seconds | number | As the server attributed (wall × 1 / concurrent sessions). |
service_ms_p50 | number | null | |
service_ms_p95 | number | null | |
chunks_applied | integer | null | As the device reported. |
rtt_ms_p50 | number | null | |
rtt_ms_p95 | number | null | |
deadline_misses | integer | null | |
degraded_count | integer | null | How many times the device's local fail-safe fired (hold on deadline miss or link loss). |
cost_usd | number | null |
|
priced | boolean | |
created_at | string (date-time) |
EndpointReportStatus
EndpointSessionState
The server's numbers for one session, replaced on every report.
| Field | Type | Description |
|---|---|---|
session_id | Id | |
state | string | |
chunks_served | integer | |
gpu_seconds | number | Wall time this session was open × 1 / the number of sessions open alongside it, summed per interval. |
service_ms_p50 | number | null | |
service_ms_p95 | number | null | |
closed_reason | string | null |