Skip to content

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. Default 1.
  • 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 on created_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 / uid and check before retrying on network failure.
  • Use the database uid field 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.