◆ 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:
| Pattern | Header / Body | Used 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:
Returns tier, feature flags, email, expiry. Cached 24h by the local app.
{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:
Returns a short-lived Stripe Checkout URL.
{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:
Issues a 14-day Pro-tier license. Emails the key. Refuses disposable-email domains and limits to 2 trials per IP per 24h.
{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
Body:
Dispatches to Resend (email) + ntfy.sh (push). Server-side 4h dedup on
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
Body:
Max size: 5 MB. Retention: newest 30 per license.
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"
}
}
| Endpoint | Limit | Window | Keyed by |
|---|---|---|---|
/api/validate-license | 120 | 1 minute | license key |
/api/alert-dispatch | 30 | 1 hour | license key |
/api/sync/upload | 10 | 24 hours | license key |
/api/create-checkout-session | 10 | 1 hour | IP address |
/api/portal/session | 10 | 1 hour | license 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
| Status | Meaning | What to do |
|---|---|---|
400 | Bad request — malformed body or invalid field values | Check the detail field for what's wrong |
401 | Missing / invalid license key (on auth-protected routes) | Re-check X-License-Key header |
403 | Valid license but tier doesn't include this feature | Upgrade, or check features dict from /validate-license |
404 | Resource not found (e.g. snapshot id) | Re-list; the id may have been deleted |
429 | Rate limit exceeded | Back off per Retry-After header |
502 | Upstream provider error (Stripe, Resend, Supabase) | Usually transient — retry after 10s |
503 | Feature not configured on this deployment | Service 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"
# }