Citra Space Edge Sensor API
The Edge Sensor API from Citra Space — 9 operation(s) for edge sensor.
The Edge Sensor API from Citra Space — 9 operation(s) for edge sensor.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/citra-space:citra-space-edge-sensor-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: SOICAT Edge Sensor API
version: 0.1.0
tags:
- name: Edge Sensor
paths:
/edge-sensor:
post:
description: 'Admin: pre-provision a sensor + mint its one-use enrollment token (returned once).'
operationId: provision_edge_sensor_edge_sensor_post
parameters:
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProvisionSensorRequest'
required: true
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/ProvisionSensorResponse'
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Provision Edge Sensor
tags:
- Edge Sensor
/edge-sensor/enroll:
post:
description: 'Enroll a sensor: validate its one-use token, sign its CSR, return the cert bundle.
On an EdgeNode deployment this instead PROXIES to central and attaches the sensor to this node
(see edge_node_local._device_enroll). Branching here rather than shadowing the route by mount
order keeps one owner for the path — and means a sensor points its api_base_url at the node with
no client-side change, since the URL shape is identical either way. A node cannot serve the
central branch regardless: it holds no step-ca provisioner password.'
operationId: enroll_edge_sensor_enroll_post
parameters:
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EnrollRequest'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/EnrollResponse'
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
security:
- HTTPBearer: []
summary: Enroll
tags:
- Edge Sensor
/edge-sensor/link:
post:
operationId: link_edge_sensor_edge_sensor_link_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EdgeSensorLinkRequest'
required: true
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/EdgeSensorLinkResponse'
description: Linked; the device enrolls with the link
'403':
description: The group is not one of the caller's
'409':
description: A chosen telescope or antenna is linked to another live device, or this node enrolls through Citra Core
'422':
description: No name, no telescope or antenna, or one that is not the caller's or their group's
'503':
description: This deployment cannot sign enrollment tokens, or has no public API address
security:
- HTTPBearer: []
summary: Link a Third-Party Sensor
tags:
- Edge Sensor
/edge-sensor/linkable-sensors:
get:
operationId: list_linkable_sensors_edge_sensor_linkable_sensors_get
parameters:
- description: 'The group the device is for: one of the caller''s. Omitted means their own group.'
in: query
name: groupId
required: false
schema:
format: uuid
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/LinkableSensors'
description: Successful Response
'403':
description: The group is not one of the caller's
security:
- HTTPBearer: []
summary: List Sensors a Device Can Serve
tags:
- Edge Sensor
/edge-sensor/renew-cert:
post:
description: 'Renew a sensor cert via key-pinned proof of possession (no token; self-authenticating, so this
route is excluded from GlobalAuthMiddleware alongside /enroll).
On an EdgeNode this PROXIES to central, for the same reason /enroll does and then some: a node
holds no step-ca credentials, and its local edge_sensor row carries no key pin — so it could
neither authorise nor sign a renewal. Serving it locally returned a TERMINAL 403 that stopped the
device''s renewer for good, which made certificate expiry a guaranteed brick for every
edge-attached sensor.'
operationId: renew_cert_edge_sensor_renew_cert_post
parameters:
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RenewRequest'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/EnrollResponse'
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Renew Cert
tags:
- Edge Sensor
/edge-sensor/task-domain:
get:
description: 'Report the JetStream domain this tier serves device tasks from.
Branches on the deployment exactly like POST /enroll above, and for the same reason: a sensor
points its API base URL at whichever tier hosts it and needs no client-side difference. The answer
is the tier''s own configuration — the value its broker actually runs and its ACLs actually grant —
so it cannot drift from reality the way a copy handed out once at enrollment does.
The gateway''s domain comes from NATS_SETTINGS rather than the "gateway" literal that
EnrollResponse defaults to, so that renaming it moves this endpoint and the gateway''s own
`$JS..API` prefix together.
Authenticated like every other sensor-facing read (a device holds a PAT; the enroll routes are
the deliberate exception because they self-authenticate with a one-use token).'
operationId: get_task_domain_edge_sensor_task_domain_get
parameters:
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TaskDomainResponse'
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Get Task Domain
tags:
- Edge Sensor
/edge-sensor/{edge_sensor_id}/attachment:
put:
description: 'Admin: re-home an enrolled sensor to another EdgeNode, or detach it back to the gateway.
Until this existed, the attachment could only be set while provisioning, so moving a sensor meant
re-enrolling it under a NEW id — which left the old row enrolled on the old node, its scheduler
still planning for a sensor that had gone, and every telescope binding pointing at the dead id.
Writing the column is the entire action: cdc replicates the row, edge-projector fans it to the
new node AND the previous one, and each turns that into an adopt or a withdraw. No token, no
re-enrollment, no re-issued certificate.
PUT rather than POST because it is idempotent — the same body applied twice is the same state.
Detaching is ``{"attachedEdgeNodeId": null}``.'
operationId: set_edge_sensor_attachment_edge_sensor__edge_sensor_id__attachment_put
parameters:
- in: path
name: edge_sensor_id
required: true
schema:
format: uuid
title: Edge Sensor Id
type: string
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SetSensorAttachmentRequest'
required: true
responses:
'204':
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Set Edge Sensor Attachment
tags:
- Edge Sensor
/edge-sensor/{edge_sensor_id}/bindings:
get:
description: 'The catalog instruments bound to this edge sensor, so a device can discover what it represents.
Served identically by both tiers: a node holds the same telescope and antenna rows, down-synced
from core, so an attached sensor asking its node gets the same answer it would get from central —
including while partitioned, which is the point.
404 on an unknown sensor rather than empty lists: "this sensor does not exist" and "this sensor
has nothing bound yet" lead an operator to entirely different places, and a device that just
enrolled has every reason to expect the former to be impossible.'
operationId: get_sensor_bindings_edge_sensor__edge_sensor_id__bindings_get
parameters:
- in: path
name: edge_sensor_id
required: true
schema:
format: uuid
title: Edge Sensor Id
type: string
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SensorBindingsResponse'
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Get Sensor Bindings
tags:
- Edge Sensor
/edge-sensor/{edge_sensor_id}/revoke:
post:
description: 'Admin: revoke a sensor''s cert — the kill switch for a stolen key (renewal turns a stolen key
into an indefinitely renewable credential). Idempotent (already-revoked -> 204); 404 on unknown
id.'
operationId: revoke_edge_sensor_edge_sensor__edge_sensor_id__revoke_post
parameters:
- in: path
name: edge_sensor_id
required: true
schema:
format: uuid
title: Edge Sensor Id
type: string
- in: header
name: X-Scenario-Context
required: false
schema:
anyOf:
- format: uuid
type: string
- type: 'null'
responses:
'204':
description: Successful Response
'404':
description: No such device the caller may revoke
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
'502':
description: The OTA repository could not be told; nothing was revoked
summary: Revoke Edge Sensor
tags:
- Edge Sensor
components:
schemas:
EnrollRequest:
description: Device-supplied body for ``POST /api/edge-sensor/enroll`` (camelCase over the wire).
properties:
csrPem:
description: PEM-encoded PKCS#10 CSR; the device keypair never leaves the device.
maxLength: 16384
title: Csrpem
type: string
deviceSerial:
description: Hardware serial the device reports at enrollment.
title: Deviceserial
type: string
hardwareReport:
anyOf:
- additionalProperties: true
type: object
- type: 'null'
description: Free-form device descriptor (firmware version, capabilities, model, ...).
required:
- deviceSerial
- csrPem
title: EnrollRequest
type: object
SensorBindingsResponse:
description: 'The catalog instruments an operator has bound to one edge_sensor.
A device learns its own ``edge_sensor_id`` at enrollment, but the telescope or antenna it
represents is a separate object bound separately — provisioning creates only the edge_sensor row,
and the binding can happen well after the device enrolled. So this is a read, not something the
enroll bundle could have carried: it can change during a device''s life, and a device that cached
it at enrollment would be wrong the moment an operator re-bound it.
Split by kind because a device maps each answer onto a local sensor of the matching type. Lists
rather than single ids: the schema permits more than one instrument per edge_sensor, and silently
returning the "first" one would be a guess dressed as an answer.'
properties:
antennaIds:
description: Antennas bound to this edge sensor.
items:
type: string
title: Antennaids
type: array
telescopeIds:
description: Telescopes bound to this edge sensor.
items:
type: string
title: Telescopeids
type: array
title: SensorBindingsResponse
type: object
LinkableSensor:
description: A telescope or antenna the caller may link to a new device.
properties:
id:
format: uuid
type: string
name:
type: string
required:
- id
- name
type: object
LinkableSensors:
description: 'The telescopes and antennas the caller may link for a group: theirs, or the group''s, that no
other live device serves.'
properties:
antennas:
items:
$ref: '#/components/schemas/LinkableSensor'
type: array
telescopes:
items:
$ref: '#/components/schemas/LinkableSensor'
type: array
required:
- telescopes
- antennas
type: object
EdgeSensorLinkResponse:
properties:
edgeSensorId:
format: uuid
type: string
enrollmentLink:
description: '`<this API>/edge-sensor/enroll#<token>`: the one-use enrollment token rides in the
fragment, so no server receives or logs it.'
type: string
expiresAt:
description: When the link's token lapses unused.
format: date-time
type: string
required:
- edgeSensorId
- enrollmentLink
- expiresAt
type: object
SetSensorAttachmentRequest:
description: Admin body for PUT /edge-sensor/{id}/attachment — re-home a sensor, or detach it.
properties:
attachedEdgeNodeId:
anyOf:
- format: uuid
type: string
- type: 'null'
description: 'The EdgeNode that should host this sensor, or null to detach it back to the platform gateway. Unlike provisioning, this takes effect on an ALREADY-ENROLLED sensor: no new token, no re-enrollment and no new certificate, because the device CN is node-independent. The new node adopts the sensor and the previous one withdraws it, both driven by the replicated catalog row. The certificate LIFETIME converges at the next renewal (attaching lengthens it, detaching shortens it).'
title: SetSensorAttachmentRequest
type: object
RenewRequest:
description: 'Device-supplied body for ``POST /api/edge-sensor/renew-cert`` (camelCase over the wire).
Bare CSR: the CSR must be signed by the key pinned at enrollment (SPKI-hash match plus a valid
self-signature = proof of possession). No token — key loss requires re-enrollment.'
properties:
csrPem:
description: PEM-encoded PKCS#10 CSR signed by the pinned device key.
maxLength: 16384
title: Csrpem
type: string
required:
- csrPem
title: RenewRequest
type: object
EdgeSensorLinkRequest:
description: 'Link a device to telescopes and antennas before it exists (monorepo ADR 0011): the fleet and
provisioning-script path. The device redeems the link''s one-use token with `limelink enroll`.'
properties:
antennaIds:
items:
format: uuid
type: string
type: array
groupId:
description: 'The group the device belongs to: one of the caller''s. Omitted means their own group.'
format: uuid
type:
- string
- 'null'
name:
description: The device's name, as Core shows it.
type: string
telescopeIds:
description: 'The telescopes it serves: the caller''s, or the group''s. At least one telescope or antenna.'
items:
format: uuid
type: string
type: array
required:
- name
type: object
ProvisionSensorRequest:
description: Admin body for POST /edge-sensor (camelCase over the wire).
properties:
attachedEdgeNodeId:
anyOf:
- format: uuid
type: string
- type: 'null'
description: 'Set when this sensor will leaf to an EdgeNode''s broker rather than the platform gateway. It travels in the one-use enrollment token as a verified claim, never in the enroll body, so a device cannot elect its own tier: it selects the longer offline-capable certificate lifetime and marks the sensor attached so core stands down from tasking it. Null means a direct sensor, which is unchanged.'
citraUserId:
format: uuid
title: Citrauserid
type: string
friendlyName:
title: Friendlyname
type: string
hardwareClass:
title: Hardwareclass
type: string
required:
- citraUserId
- friendlyName
- hardwareClass
title: ProvisionSensorRequest
type: object
ProvisionedSensor:
properties:
citraUserId:
format: uuid
title: Citrauserid
type: string
createdAt:
format: date-time
title: Createdat
type: string
friendlyName:
title: Friendlyname
type: string
hardwareClass:
title: Hardwareclass
type: string
id:
format: uuid
title: Id
type: string
required:
- id
- citraUserId
- friendlyName
- hardwareClass
- createdAt
title: ProvisionedSensor
type: object
TaskDomainResponse:
description: 'The JetStream domain THIS tier serves tasks from, for a sensor to reconcile against.
Exists because ``js_domain`` was distributed exactly once, in the enroll bundle, and a node''s
domain is derived from its id — so replacing a node''s identity left every attached sensor pulling
from a domain that no longer exists, with no way to be told. Uploads kept working (they address
the DEVICE''s own domain), which made it a silent half-failure. A sensor now re-reads this at
startup and treats the tier as authoritative.
Deliberately domain ONLY. The leaf URL is not served here even though it is the other fact that
can go stale: a sensor must already be connected to ask, so it cannot bootstrap from this, and
letting an API response re-point which broker a device dials is a strictly worse failure mode than
a stale domain. Nothing here is a secret either — the domain is derivable from the node id that
appears in its certificate — and the broker ACL, not this string, is what authorizes a pull.'
properties:
jsDomain:
description: JetStream domain this tier serves device tasks from.
title: Jsdomain
type: string
required:
- jsDomain
title: TaskDomainResponse
type: object
ProvisionSensorResponse:
description: The created device plus the one-use enrollment token (shown once; never logged).
properties:
device:
$ref: '#/components/schemas/ProvisionedSensor'
enrollmentToken:
title: Enrollmenttoken
type: string
required:
- device
- enrollmentToken
title: ProvisionSensorResponse
type: object
ValidationError:
properties:
ctx:
type: object
input: {}
loc:
items:
oneOf:
- type: string
- format: int64
type: integer
type: array
msg:
type: string
type:
type: string
required:
- loc
- msg
- type
type: object
EnrollResponse:
description: 'Device leaf package returned to a successfully enrolled device.
The device combines `leaf_cert_pem` + its locally-held private key (the key never left the
device) into its local NATS leaf-node config and dials `edge_leaf_url` (the hub LEAFNODE port).'
properties:
caRootPem:
title: Carootpem
type: string
edgeLeafUrl:
title: Edgeleafurl
type: string
edgeSensorId:
title: Edgesensorid
type: string
jsDomain:
default: gateway
title: Jsdomain
type: string
leafCertChainPem:
title: Leafcertchainpem
type: string
leafCertPem:
title: Leafcertpem
type: string
required:
- leafCertPem
- leafCertChainPem
- caRootPem
- edgeLeafUrl
- edgeSensorId
title: EnrollResponse
type: object
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
type: array
type: object
securitySchemes:
HTTPBearer:
scheme: bearer
type: http