Spinwarden · API reference

API reference

Spinwarden exposes a small REST surface and a webhook event stream. Every endpoint you see on the console — telemetry, schedule contacts, downlink prioritization, anomaly ack, fleet snapshot, compliance manifest, PDF filing — is also a route handler you can hit from curl. Every state change the console watches — pass scheduled, downlink queued, anomaly flagged, FCC/ITU filed — ships as a webhook with the same shape across providers.

Use this reference to plug Spinwarden into scripts, CI, or a downstream tool. The bridge table at the bottom shows exactly how our normalized event shape maps to Azure Orbital and AWS Ground Station webhooks so a team already running one of those can adopt Spinwarden without rewriting their ingest.

REST · auth

Authentication

REST endpoints accept a bearer ingest token in the Authorization header. The token is per-tenant and rotated through the operator console; treat it like any other API key — never commit it, scope it to a single CI job or worker, and rotate quarterly. Webhooks are signed separately with the per-tenant signing secret — see Verification below.

  • Header: Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>
  • Scope: read-only for snapshots, write-only for ingest + anomaly ack
  • Rotation: console → Tenant → Rotate (older tokens stay valid for 24h)

POST · /api/telemetry/ingest

Telemetry ingest

Push a raw telemetry frame into the Spinwarden pipeline. The handler validates the envelope, attaches a server-side receivedAt, and feeds the frame into the same parser the mission-ops console reads — so a curl POST shows up in the viewer within ~2 seconds.

POST — push one frame
curl -X POST https://api.spinwarden.io/api/telemetry/ingest -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" -H "Content-Type: application/json" -d '{"satelliteId":"spinwarden-01","seq":42,"raw":"AAECAwQFBgcICQ==","fields":[{"key":"voltage","value":4.12},{"key":"temp_c","value":-8.4}]}'

GET · /api/schedule/contacts

Schedule — list upcoming contacts

Returns every pass intersecting the next 24-hour window. AOS before the window closes AND LOS after the request time, so a pass currently in progress stays in the list. Sorted by AOS ascending; same shape as the Pass contract under src/lib/contracts.

GET — full list
curl https://api.spinwarden.io/api/schedule/contacts -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"
GET — jq filter to one provider
curl -s https://api.spinwarden.io/api/schedule/contacts -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" | jq '.items[] | select(.provider == "aws-ground-station")'

GET · /api/downlink

Downlink queue in mission-value order. Each row is the DownlinkRow schema: id, payloadName, missionValueScore (0–100), groundStation, queuedAt, windowSeconds. Use ≥ 70 as a hard SLA trigger; below 30 marks a "best-effort" pass you can skip.

GET — full queue
curl https://api.spinwarden.io/api/downlink -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"
GET — only mission-critical (score ≥ 70)
curl -s https://api.spinwarden.io/api/downlink -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" | jq '[.items[] | select(.missionValueScore >= 70)]'

GET · /api/anomalies · POST · /api/anomalies/[id]/ack

Anomalies — list + ack

Open anomalies first, then most-recent acknowledged ones. Ack returns the full Anomaly shape with acknowledgedAt set to ISO-now; idempotent — re-acking is a no-op (still returns the row, no error).

GET — list
curl https://api.spinwarden.io/api/anomalies -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"
POST — ack one anomaly
curl -X POST https://api.spinwarden.io/api/anomalies/anm_3f6c1b/ack -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" -H "Content-Type: application/json" -d '{}'

GET · /api/fleet/snapshot

Fleet snapshot

Three scalars for the home dashboard: satellite count, next pass start time, and the time the snapshot was fetched (server-side receivedAt). Designed for cheap polling — no joins, no nested rows, safe to refresh every 30s.

GET — full snapshot
curl https://api.spinwarden.io/api/fleet/snapshot -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"

GET · /api/compliance/manifest

Compliance — fleet manifest

Returns the operator-supplied FleetManifest JSON — the same shape the onboarding wizard parses. Useful for reconciling Spinwarden against your own ground-truth inventory.

GET — raw manifest
curl https://api.spinwarden.io/api/compliance/manifest -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"

GET · /api/compliance/filing.pdf

Compliance — filing PDF

Generate the per-regulator filing draft (FCC Schedule-S or ITU AP30B / Appendix 4) straight from the FleetManifest. Returns a stream of application/pdf bytes, ready for download. Pass the format as a query string and provide the manifest in the JSON body.

POST — FCC draft
curl -X POST 'https://api.spinwarden.io/api/compliance/filing.pdf?format=fcc' -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" -H "Content-Type: application/json" -d '{"operatorName":"Spinwarden Test Operator","transmitterCountry":"US","frequencyPlan":"X-band down / S-band up","spacecraftIds":["spinwarden-01","spinwarden-02"]}' --output spinwarden-fcc.pdf
POST — ITU draft
curl -X POST 'https://api.spinwarden.io/api/compliance/filing.pdf?format=itu' -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" -H "Content-Type: application/json" -d '{"operatorName":"Spinwarden Test Operator","transmitterCountry":"US","frequencyPlan":"X-band down / S-band up","spacecraftIds":["spinwarden-01","spinwarden-02"]}' --output spinwarden-itu.pdf

POST · /api/pdf/document

PDF document — generic builder

Lower-level endpoint — POST any DocumentSpec and stream a PDF. The route handler validates the body against the DocumentSpec schema from the pdf module and returns the rendered bytes. Use this when you already have a document spec and want to bypass the per-endpoint wiring.

POST — custom DocumentSpec
curl -X POST https://api.spinwarden.io/api/pdf/document -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>" -H "Content-Type: application/json" -d '{"title":"Weekly Downlink Summary","subtitle":"W42 fleet-wide","meta":[{"label":"Window","value":"2026-10-12 → 2026-10-19"}],"lineItems":[{"description":"Downlink queue length","quantity":120,"unitAmountCents":0}],"notes":"Auto-generated weekly brief.","footer":"Spinwarden W42"}' --output spinwarden-w42.pdf

POST · your subscription URL

Webhook events

Spinwarden posts a normalized event for every state change the console watches. Every payload carries the same envelope — event, satelliteId, groundStation, timestamp fields — so a single dispatcher handles all five events below. Azure Orbital + AWS Ground Station comparison lives in the bridge section further down.

pass.scheduled

Emitted when a satellite pass enters the scheduling window — typically a few minutes before AOS. Use this to warm your decoder; the pass will go live within the next scheduled windowStart / windowEnd.

Webhook payload — pass.scheduled
curl -X POST https://your.endpoint/webhooks/spinwarden -H "X-Spinwarden-Signature: t=1700000000,v1=<hex>" -H "X-Spinwarden-Delivery: 8d6f1f4a-9c2b-4e0a-9b3f-1c5b1eafb9b1" -H "Content-Type: application/json" -d '{"event":"pass.scheduled","satelliteId":"spinwarden-01","groundStation":"aws-ground-station:us-east-1","windowStart":"2026-08-06T12:34:00Z","windowEnd":"2026-08-06T12:39:30Z"}'

pass.completed

Emitted when a pass closes — uplink/downlink bytes attributed to the pass, plus a quality score 0–100 derived from signal margins. A failed pass (no frames received) still emits this event with downlinkBytes=0 and qualityScore=0.

Webhook payload — pass.completed
curl -X POST https://your.endpoint/webhooks/spinwarden -H "X-Spinwarden-Signature: t=1700000400,v1=<hex>" -H "X-Spinwarden-Delivery: 9e5e2c40-7e6f-4c12-9c81-7e6f4c5b9e6f" -H "Content-Type: application/json" -d '{"event":"pass.completed","satelliteId":"spinwarden-01","groundStation":"aws-ground-station:us-east-1","windowStart":"2026-08-06T12:34:00Z","windowEnd":"2026-08-06T12:39:30Z","downlinkBytes":4194304,"qualityScore":92}'

Emitted when a queued downlink is fully downlinked and made available through the /api/downlink endpoint. Carries the queue row id from DownlinkRow so you can reconcile back to the queue snapshot.

Webhook payload — downlink.completed
curl -X POST https://your.endpoint/webhooks/spinwarden -H "X-Spinwarden-Signature: t=1700000800,v1=<hex>" -H "X-Spinwarden-Delivery: a7c0e2b6-3e6d-4b5a-b7e0-3c5b9e7ad4a1" -H "Content-Type: application/json" -d '{"event":"downlink.completed","id":"dl_4f0a7b","payloadName":"downlink:pass-2026-08-06T12-39Z","queuedAt":"2026-08-06T12:38:00Z","groundStation":"aws-ground-station:us-east-1"}'

anomaly.detected

Emitted when the anomaly engine flags a frame — severity (info/warning/critical), category (thermal/telemetry-loss/link-budget/…), and the affected satellite. Acknowledge from the console or via POST /api/anomalies/[id]/ack.

Webhook payload — anomaly.detected
curl -X POST https://your.endpoint/webhooks/spinwarden -H "X-Spinwarden-Signature: t=1700001000,v1=<hex>" -H "X-Spinwarden-Delivery: b2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f" -H "Content-Type: application/json" -d '{"event":"anomaly.detected","id":"anm_3f6c1b","severity":"warning","category":"link-budget","satelliteId":"spinwarden-02"}'

compliance.filed

Emitted when Spinwarden successfully files an FCC or ITU submission on your behalf. Carries the regulator, the filing reference returned by the regulator, and the ISO timestamp it was filed.

Webhook payload — compliance.filed
curl -X POST https://your.endpoint/webhooks/spinwarden -H "X-Spinwarden-Signature: t=1700001400,v1=<hex>" -H "X-Spinwarden-Delivery: c5b8a9d2-4e6f-4a7b-8c9d-0e1f2a3b4c5d" -H "Content-Type: application/json" -d '{"event":"compliance.filed","id":"cmp_91d4a7","regulator":"FCC","filingReference":"SAT-PDR-20260806-014","filedAt":"2026-08-06T13:00:00Z"}'

webhook · HMAC

Verification

Every webhook carries two headers for replay + authenticity. X-Spinwarden-Signature is the HMAC SHA-256 of t=<unix_ts>.<raw body> using your per-tenant signing secret. X-Spinwarden-Delivery is a unique id you can log to dedupe redeliveries.

Verify — node snippet
node -e "const c=require('crypto');const t=process.argv[1];const raw=process.argv[2];const sec='<SPINWARDEN_WEBHOOK_SECRET>';const v=c.createHmac('sha256',sec).update(t+'.'+raw).digest('hex');console.log('expected:', 'v1='+v);" 1700000000 "$(cat payload.json)"

Compatibility reference

GSaaS bridge — Azure Orbital + AWS Ground Station

Spinwarden's webhook shape is the normalized events table below. If your team is already on a hyperscaler GSaaS, this is the field-by-field bridge — Azure Orbital and AWS Ground Station each publish their own webhook schema with a different grammar, and the table shows exactly how a Spinwarden event maps onto each.

Both providers stream SpacecraftEvent / contact-status webhooks with provider-managed ARN identifiers; Spinwarden emit a normalized satelliteId + groundStation string. The bridge below is the work you save by routing both through us.

Field-by-field — Spinwarden → Azure Orbital → AWS Ground Station

Field-by-field — Spinwarden → Azure Orbital → AWS Ground Station

Read left-to-right: Spinwarden ship the normalized event shape that the Spinwarden dispatcher in your code reads once. Azure Orbital and AWS Ground Station each carry provider-specific fields — ARNs, operation windows, satellite / contact-profile identifiers. This table is the concrete translation.

Spinwarden (normalized)Azure OrbitalAWS Ground Station
pass.scheduled.windowStart (ISO-8601)SpacecraftEvent.metadata.operationStartTimecontact-status.payloadWindowEndTime — note AWS only emits the END time; reverse-map by subtracting the predicted window duration.
pass.scheduled.windowEnd (ISO-8601)SpacecraftEvent.metadata.operationEndTimecontact-status.payloadWindowEndTime
pass.scheduled.satelliteId (Spinwarden id)SpacecraftEvent.spacecraftId — Azure ARN, e.g. /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Orbital/spacecrafts/<name>contact-status.satelliteArn — AWS ARN, e.g. arn:aws:groundstation:us-east-2:123456789012:satellite/<id>
pass.scheduled.groundStation (string)ContactProfile.id — Azure resource id, split on /subscriptions/.../contactProfile/<region>/<name>contactProfileArn — AWS ARN of the contact profile
pass.scheduled → pass.completed (separate event)Two SpacecraftEvent payloads: one for start (In-Spaceport) and one for end (SpacecraftEvent with operationEndTime)Single contact-status webhooks per state transition (SCHEDULED, AWS_GROUND_STATION_CONTACT_RESOLVED, COMPLETED)
pass.completed.downlinkBytesNo native event — derive by summing the per-event transfer size on each SpacecraftEventNo native event — derive by reading contact-status eventStatus + the downlink window length
downlink.completed (event + queue row id)No direct equivalent — Azure Orbital does not model a per-downlink queue; you see end-of-pass onlyNo direct equivalent — AWS emits per-pass completion, not per-downlink
anomaly.detected (Spinwarden-only)Not provided — Azure does not emit an anomaly signal; Spinwarden emits a derived alert insteadNot provided — AWS does not emit an anomaly signal; Spinwarden emits a derived alert instead
compliance.filed (Spinwarden-only)Not provided — Azure has no FCC/ITU filing surface; teams build it themselvesNot provided — AWS has no FCC/ITU filing surface; teams build it themselves
auth: bearer ingest token (header)OAuth2 client-credentials to management.azure.com + ARM RBAC on the spacecraft resourceSigV4 with IAM role + groundstation:CreateDataflowEndpointGroup
webhook signing: X-Spinwarden-Signature (HMAC SHA-256)No signing — rely on private endpoints + Azure AD authenticationNo signing — rely on private endpoints + IAM-scoped API keys

Gap rows (downlink.completed, anomaly.detected, compliance.filed) are Spinwarden-only — hyperscaler GSaaS providers do not model them. If your team is consolidating onto Spinwarden, the bridge is the work you skip.

Webhook · verification primer

Verify a webhook signature

Every Spinwarden webhook posts two headers. X-Spinwarden-Signature carries t=<unix>,v1=<hex> — the v1 hex is the HMAC SHA-256 of "<t>.<raw body>" keyed by your per-tenant signing secret. X-Spinwarden-Delivery is the unique delivery id. Reject any delivery older than 5 minutes; redeliveries should be deduped by delivery id.

Verify — node one-liner (HMAC SHA-256 over t.<raw>)
node -e "const c=require('crypto');const t=process.argv[1];const raw=process.argv[2];const sec='<SPINWARDEN_WEBHOOK_SECRET>';const v=c.createHmac('sha256',sec).update(t+'.'+raw).digest('hex');console.log('expected:', 'v1='+v);console.log('compare against the X-Spinwarden-Signature header you received');" 1700000000 "$(cat payload.json)"