AllAccess API

Backend for radio.com/all-access. JSON over HTTPS. Auth via Keycloak. Billing via Stripe. Trebel Max receives a daily SFTP file.

How it fits together

Keycloak holds the user and the allaccess_enddate attribute. Stripe is what flips entitlement on and off. When a webhook lands we update that attribute. SeatClub reads the flag at SSO. Trebel gets a daily SFTP file of active emails (backend-only, no frontend endpoint).

We're just the glue. No DB of our own.

Auth

The frontend logs into Keycloak directly with keycloak-js, then calls this API with the access token:

Authorization: Bearer <keycloak-access-token>

We verify against Keycloak's JWKS. No roundtrip on every request.

Endpoints

GET/healthz

Liveness check. Returns {"status": "ok"}.

GET/readyz

Readiness check.

POST/signup

Creates a Keycloak user, sends the verification email, creates a Stripe customer, and returns an Embedded Checkout client_secret.

Body

{
  "first_name": "Ada",
  "last_name": "Lovelace",
  "email": "ada@example.com",
  "password": "<min 8 chars>"
}

Response

{
  "user_id": "f3a1...",
  "client_secret": "cs_test_..._secret_..."
}

Frontend mounts Stripe's Embedded Checkout with this client_secret.

POST/signup/resend-verification

Resends the Keycloak verification email. Returns {"ok": true} either way. If we 404'd on unknown emails it'd leak which addresses are members. Rate-limited 5/hr per IP.

{ "email": "ada@example.com" }
POST/password-reset/request

Kicks off Keycloak's "update password" required action and emails the user a link. Same silent-on-unknown-email behavior + rate limit as above.

{ "email": "ada@example.com" }
GET/merequires bearer token

Profile + entitlements for the calling user.

{
  "id": "f3a1...",
  "email": "ada@example.com",
  "first_name": "Ada",
  "last_name": "Lovelace",
  "member_since": "2026-05-06",
  "end_date": "2027-05-05",
  "total_orders": 1,
  "allaccess": true,
  "seatclub": true,
  "trebel_max": true
}
PATCH/merequires bearer token

Update first/last name. Returns the same shape as GET /me.

{ "first_name": "Ada", "last_name": "Lovelace" }
GET/entitlementsrequires bearer token

Just the entitlement flags. Useful when the SeatClub white-label or any other integrator wants the bare check.

{
  "end_date": "2027-05-05",
  "allaccess": true,
  "seatclub": true,
  "trebel_max": true
}
POST/billing/checkout-sessionrequires bearer token

Returns a fresh Stripe Embedded Checkout client_secret. Used when the user is signed in but hasn't finished paying.

{ "client_secret": "cs_test_..._secret_..." }
POST/billing/portal-sessionrequires bearer token

Returns a Stripe Customer Portal URL. Drives the "Manage Plan" button. Stripe doesn't offer an embedded portal so this stays a redirect.

{ "url": "https://billing.stripe.com/p/session/..." }
GET/billing/subscriptionrequires bearer token

Subscription details for the Settings page (status, next bill date, price). Returns {"subscription": null} if the user hasn't paid yet.

{
  "subscription": {
    "id": "sub_...",
    "status": "active",
    "current_period_end": 1773926400,
    "cancel_at_period_end": false,
    "amount": 14900,
    "currency": "usd",
    "interval": "year"
  }
}
GET/billing/ordersrequires bearer token

Recent invoices for the order history page.

{
  "orders": [
    {
      "id": "in_...", "number": "AA-0001",
      "amount_paid": 14900, "currency": "usd",
      "status": "paid", "created": 1741200000,
      "hosted_invoice_url": "https://...", "invoice_pdf": "https://..."
    }
  ]
}
POST/webhooks/stripe

Stripe-signed webhook. Idempotent. Handled events:

Errors

All errors share one shape:

{
  "code": "AA-AUTH-002",
  "message": "Token expired",
  "request_id": "..."
}
CodeMeaning
AA-AUTH-001Token invalid
AA-AUTH-002Token expired
AA-AUTH-003Signature mismatch
AA-AUTH-004Audience mismatch
AA-ENT-001No active membership
AA-ENT-002Membership expired
AA-USER-001User not found
AA-USER-002User mismatch
AA-PAY-001Stripe customer create failed
AA-PAY-002Webhook signature invalid
AA-TRBL-001SFTP upload failed

Out of scope (deliberate)

Stuff we left off on purpose: admin/support endpoints (manual entitlement grants, user lookup, webhook replay), deep health checks that ping downstreams, version endpoint, Sentry, GDPR data export, server-side promo code validation. Each touches a call Bryan or product owns. Easy to add later if a real need shows up.

Playground

Real Keycloak and Stripe aren't required to try the API. Open the playground to step through signup, payment, entitlements, and Trebel handoff against a sandboxed in-memory copy at /demo/*.


Source: allaccess-backend. Plan: ../GAMEPLAN.md.