Conventions
Rules that apply across endpoints. Worth skimming before you start building.
Wire format — camelCase JSON
The Python API runs through
fastapi_camelcase,
which auto-converts Python's snake_case field names to camelCase on
the wire. So a Python model field observatory_id shows up as
observatoryId in JSON request/response bodies and query strings.
The TypeScript SDK reflects the wire format: types are camelCase.
The Python SDK uses snake_case (matching the Python models); when
talking to the API directly with httpx, pass camelCase keys in
the body.
Resource URIs
All REST endpoints sit under /v1/:
GET /v1/observations
GET /v1/observations/{id}
POST /v1/observations
PATCH /v1/observations/{id}
DELETE /v1/observations/{id}
A handful of operations live as action endpoints under the
resource — e.g., POST /v1/observations/{id}/publish. These take
no body or take a small action-specific payload.
{id} is usually the integer primary key. Records that travel
between systems and through the WebSocket use uid (a UUID) — both
are valid lookups on most resources.
Polymorphic resources and discriminators
Several core resources are polymorphic — they share a base type and specialize through concrete subclasses. Each carries a discriminator field in JSON that tells you which concrete kind you're looking at.
| Resource family | Discriminator (wire) | Discriminator (Python) |
|---|---|---|
Device |
deviceType |
device_type |
Instrument |
instrumentType |
instrument_type |
ObservationTask |
taskType |
task_type |
ObservationRequest |
requestType |
request_type |
Target position |
positionType |
position_type |
ObservingGrant |
grantType |
grant_type |
Event |
eventType |
event_type |
Entity |
entityType |
entity_type |
OperationalConstraint |
constraintType |
constraint_type |
Coordinates |
coordinateType |
coordinate_type |
DataTool |
dataToolType |
data_tool_type |
FilterSpecifier |
filterSpecifierType |
filter_specifier_type |
Invitation |
invitationType |
invitation_type |
LogEntry |
logEntryType |
log_entry_type |
DeviceModel |
modelType |
model_type |
SpectralComponent |
spectralComponentType |
spectral_component_type |
TemporalComponent |
temporalComponentType |
temporal_component_type |
CatalogObject |
catalogObjectType |
catalog_object_type |
ResourceRef |
resourceType |
resource_type |
Integration |
integrationType |
integration_type |
Observation |
kind |
kind |
Message (WS) |
kind |
kind |
When unmarshaling polymorphic JSON, switch on the discriminator first, then validate against the concrete subclass. The SDKs (both TS and Python) do this for you.
Pagination
List endpoints return a Page<T> envelope:
{
"items": [ /* T[] */ ],
"total": 1234,
"page": 1,
"size": 50,
"pages": 25
}
Query params:
page— 1-based page number. Default1.size— items per page. Defaults are per-endpoint; if you need a specific upper bound, check the OpenAPI spec for that endpoint.
Some endpoints (mostly graph endpoints — see apps/public-api/GRAPH_API_TRACKER.md) return a richer envelope that includes related resources by uid. That shape is documented per endpoint.
Filtering and sorting
Filter params are documented per endpoint in the OpenAPI spec. Most list endpoints support:
owner_id— restrict to records owned by a specific entity.created_after/created_before— time-range filters oncreated_on.is_public— filter by visibility.
Sorting is via order_by=field and order=asc|desc where supported.
Default ordering is endpoint-specific; usually created_on desc.
Error model
REST errors come back as FastAPI's standard envelope:
{ "detail": "<error message>" }
For validation errors (422 Unprocessable Entity), FastAPI returns its richer per-field envelope:
{
"detail": [
{
"type": "missing",
"loc": ["body", "owner_id"],
"msg": "Field required",
"input": null
}
]
}
The WebSocket protocol has its own richer error envelope —
{ kind: "error", code, message, details, id } — with code drawn
from invalid, forbidden, not_found, conflict, internal.
See WebSocket protocol.
Idempotency
The platform doesn't currently advertise an Idempotency-Key
header. POST endpoints that create resources are not idempotent —
if you retry blindly, you may end up with duplicates. Strategies:
- Use the action endpoints (
POST /v1/observations/{id}/publish) which are idempotent because the state transition is. - Capture the response's
id/uidand check before retrying on network failure. - Use the database
uidfield to detect duplicates on your side if you're generating UUIDs client-side.
Rate limits
Skynet does not currently impose published rate limits on the public API. Practice basic civility — back off on 5xx, batch where possible, avoid tight polling loops in favor of WebSocket subscriptions. If you have a use case that requires high request volume, talk to the team first.
Timestamps
All timestamps are ISO 8601 UTC. The TS SDK rewrites date-time
fields to JS Date (see
packages/ts/skynet-sdk/src/index.ts);
on the Python side, models use datetime.datetime directly.
Soft delete
Many records carry an is_deleted flag rather than being removed.
List endpoints default to filtering out deleted records; restoring
a soft-deleted record is per-resource.