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.
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.
curl https://api.spinwarden.io/api/schedule/contacts -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"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 — prioritized downlinks
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.
curl https://api.spinwarden.io/api/downlink -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"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).
curl https://api.spinwarden.io/api/anomalies -H "Authorization: Bearer <SPINWARDEN_INGEST_TOKEN>"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.
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.
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.
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.pdfcurl -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.pdfPOST · /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.
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.pdfPOST · 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.
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.
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}'downlink.completed
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.
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.
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.
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.
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 Orbital | AWS Ground Station |
|---|---|---|
| pass.scheduled.windowStart (ISO-8601) | SpacecraftEvent.metadata.operationStartTime | contact-status.payloadWindowEndTime — note AWS only emits the END time; reverse-map by subtracting the predicted window duration. |
| pass.scheduled.windowEnd (ISO-8601) | SpacecraftEvent.metadata.operationEndTime | contact-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.downlinkBytes | No native event — derive by summing the per-event transfer size on each SpacecraftEvent | No 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 only | No 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 instead | Not 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 themselves | Not 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 resource | SigV4 with IAM role + groundstation:CreateDataflowEndpointGroup |
| webhook signing: X-Spinwarden-Signature (HMAC SHA-256) | No signing — rely on private endpoints + Azure AD authentication | No 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.
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)"