◆ REFERENCE

Cloud API reference

Every public endpoint on api.swing-deck.com. Most endpoints are called by the local dashboard automatically — this reference is for custom integrations or if you're auditing our behavior.

Base URL

Production: https://api.swing-deck.com

Interactive OpenAPI: api.swing-deck.com/docs (FastAPI auto-generated)

Authentication

Three auth patterns exist. Use whichever the endpoint requires:

PatternHeader / BodyUsed by
License key X-License-Key: SWING-XXXX-XXXX-XXXX All user-data endpoints (alerts, sync, portal, referrals)
None Public health, version check, checkout creation, trial signup
Cron secret Authorization: Bearer <CRON_SECRET> Internal: /api/cron/* endpoints

Endpoints

System

GET/health
Liveness probe — returns status=ok and a timestamp. Used by uptime monitors and Railway's health check.
GET/api/metrics
Public status — health of each dependent subsystem (database, billing, email). Drives swing-deck.com/status. No auth, no sensitive data.
GET/api/latest-version
Returns the currently-shipping version, minimum supported version (for force-upgrade), and download URLs per OS.

License

POST/api/validate-license
Body: {key: "SWING-XXXX-XXXX-XXXX"} — or pass via X-License-Key header.
Returns tier, feature flags, email, expiry. Cached 24h by the local app.

Billing

POST/api/create-checkout-session
Body: {plan, email?, referral_code?, success_url?, cancel_url?}.
plan: one of pro_monthly, pro_yearly, premium_monthly, premium_yearly.
Returns a short-lived Stripe Checkout URL.
POST/api/portal/session
Requires X-License-Key. Returns a Stripe Billing Portal URL where the user can update their card, change plan, cancel, and download invoices.
POST/api/stripe-webhook
Internal — Stripe calls this on checkout.session.completed, customer.subscription.deleted, invoice.payment_failed, customer.subscription.updated. Verifies signature against STRIPE_WEBHOOK_SECRET.

Trial

POST/api/trial/start
Body: {email, source?}. No credit card required.
Issues a 14-day Pro-tier license. Emails the key. Refuses disposable-email domains and limits to 2 trials per IP per 24h.

Alerts

POST/api/alert-dispatch
Requires X-License-Key with alerts_email or alerts_push feature.
Body: {alert_type, ticker?, message, details?, severity}.
Dispatches to Resend (email) + ntfy.sh (push). Server-side 4h dedup on (email, alert_type, ticker).

Cloud sync

POST/api/sync/upload
Requires X-License-Key with cloud_sync feature.
Body: {blob, meta, passphrase_fp} where blob is base64(nonce || ciphertext || tag) encrypted client-side with AES-256-GCM.
Max size: 5 MB. Retention: newest 30 per license.
GET/api/sync/list
Returns snapshot metadata (no ciphertext) for the licensed user — up to 30 rows.
GET/api/sync/download/{snapshot_id}
Returns the encrypted blob. Ownership enforced — a license key cannot download another user's snapshot.
DELETE/api/sync/{snapshot_id}
Delete a snapshot permanently. Cannot be undone.

Referrals

GET/api/referrals/stats
Requires X-License-Key. Returns referral code, share URL, total referred, credits earned, recent events.

Rate limits

In-memory token bucket per (bucket, license_key_or_ip). Resets on deploy. Exceeding the limit returns 429 with a Retry-After header and a JSON body:

{
  "detail": {
    "error":       "rate_limit_exceeded",
    "bucket":      "alert_dispatch",
    "limit":       30,
    "window_sec":  3600,
    "retry_after": 120,
    "message":     "Rate limit: 30 per 1h"
  }
}
EndpointLimitWindowKeyed by
/api/validate-license1201 minutelicense key
/api/alert-dispatch301 hourlicense key
/api/sync/upload1024 hourslicense key
/api/create-checkout-session101 hourIP address
/api/portal/session101 hourlicense key
⬡ WHY THESE LIMITS The alert-dispatch limit is the tightest — 30/hr is well above what any sane configuration would trigger, but well below what a runaway client could spam Resend with. If your legitimate use case exceeds a bucket, email support and we'll look at it.

Error codes

StatusMeaningWhat to do
400Bad request — malformed body or invalid field valuesCheck the detail field for what's wrong
401Missing / invalid license key (on auth-protected routes)Re-check X-License-Key header
403Valid license but tier doesn't include this featureUpgrade, or check features dict from /validate-license
404Resource not found (e.g. snapshot id)Re-list; the id may have been deleted
429Rate limit exceededBack off per Retry-After header
502Upstream provider error (Stripe, Resend, Supabase)Usually transient — retry after 10s
503Feature not configured on this deploymentService is live but missing an env var (Stripe key, Admin secret, etc.)

Example — validating a key from bash

curl -X POST https://api.swing-deck.com/api/validate-license \
  -H "Content-Type: application/json" \
  -d '{"key": "SWING-XXXX-XXXX-XXXX"}'

# =>
# {
#   "valid": true,
#   "tier":  "pro",
#   "features": {"max_tickers": 25, "broker_writes": true, …},
#   "email":  "you@example.com",
#   "expires_at": null,
#   "days_remaining": null,
#   "message": "Active pro license"
# }