Stripe Integration
Loquent uses Stripe for payment processing, subscription management, and billing portal access. The integration is built on the async-stripe crate and raw Axum routes for webhook handling.
Webhook Receiver
Section titled “Webhook Receiver”The webhook endpoint at POST /api/billing/stripe/webhook is a raw Axum route (not a Dioxus server function) because signature verification requires the untouched request body bytes.
Signature Verification
Section titled “Signature Verification”The handler implements Stripe’s HMAC-SHA256 signature scheme:
- Extract
t=<timestamp>andv1=<hex>entries from theStripe-Signatureheader - Compute
HMAC-SHA256(webhook_secret, "{timestamp}.{body}") - Constant-time compare against all
v1signatures - Reject if the timestamp is older than 5 minutes
Event Dispatch
Section titled “Event Dispatch”Verified events are routed by dispatch_stripe_event():
| Stripe Event | Handler |
|---|---|
customer.subscription.created | Upserts organization_subscription + creates org_budget |
customer.subscription.updated | Updates status, seats, tier, and period |
customer.subscription.deleted | Marks subscription as canceled |
invoice.paid | Resets cycle budget (replenishes included credits) |
checkout.session.completed | Handles post-checkout setup |
| All others | Logged and acknowledged (200 OK) |
All handlers are idempotent — safe for Stripe to redeliver on transient failures (the server returns 5xx to trigger retries).
Subscription Upsert
Section titled “Subscription Upsert”On customer.subscription.created/updated, the handler:
- Extracts
stripe_customer_id→ looks up the org - Reads the Stripe Price ID from the subscription item → calls
stripe::Price::retrieve()to get the parentproductfield → matches to asubscription_tierbystripe_product_id - Inserts or updates
organization_subscriptionwith status, seat count, and billing period - On create: initializes
org_budgetwith the tier’s monthly allowances
The handler reads subscription item fields (current_period_start, current_period_end, quantity, price.id) from the first item in items.data, falling back to root-level fields for backward compatibility with older Stripe API versions.
Subscription Status Mapping
Section titled “Subscription Status Mapping”Stripe statuses are collapsed into four values:
pub enum SubscriptionStatus { Trialing, // Trial period — billable Active, // Paid and current — billable PastDue, // Payment failed — not billable Canceled, // Subscription ended — not billable}incomplete, incomplete_expired, unpaid, and paused all map to PastDue.
Checkout Flow
Section titled “Checkout Flow”- The org owner selects a tier and billing interval on the billing settings page
- Frontend calls
POST /api/billing/checkoutwith the tier slug andbilling_interval(monthoryear) - Server fetches active Stripe Prices for the tier’s
stripe_product_idand resolves the Price matching the requested interval - Creates a Stripe Checkout Session via
create_checkout_session()with the resolved Price ID - Returns a
StripeRedirect { url }— the browser navigates to Stripe’s hosted page - Stripe delivers
customer.subscription.createdwebhook → subscription and budget are provisioned
The server rejects checkout requests when no active Price exists for the requested interval (400 Bad Request). Only organization owners can initiate checkout.
Billing Portal
Section titled “Billing Portal”POST /api/billing/portal creates a Stripe Billing Portal session. The portal handles plan changes, payment method updates, invoice history, and cancellation — no custom UI needed.
API Endpoints
Section titled “API Endpoints”| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/billing/tiers | Public | List active subscription tiers |
GET | /api/billing/subscription | Session | Current org’s subscription summary |
POST | /api/billing/checkout | Owner | Create Stripe Checkout Session |
POST | /api/billing/portal | Owner | Create Stripe Billing Portal Session |
POST | /api/billing/stripe/webhook | Stripe signature | Webhook receiver |
GET /api/billing/tiers
Section titled “GET /api/billing/tiers”Returns Vec<SubscriptionTierInfo>. Prices are fetched live from Stripe on each request — the database stores only the stripe_product_id, not price data.
pub struct SubscriptionTierInfo { pub id: Uuid, pub slug: String, pub name: String, pub prices: Vec<TierPrice>, pub credits_per_seat: i32, pub autonomous_plans_enabled: bool, pub knowledge_base_enabled: bool, pub custom_analyzers_enabled: bool, pub call_analysis_level: CallAnalysisLevel, // Basic | Full pub is_checkout_ready: bool, // derived: !prices.is_empty()}
pub struct TierPrice { pub stripe_price_id: String, pub interval: BillingInterval, pub unit_amount_cents: i64, pub currency: String,}
pub enum BillingInterval { Month, Year,}stripe_product_id is intentionally excluded from this public type for security. is_checkout_ready is now derived from whether the tier has any active Stripe Prices (!prices.is_empty()).
If the Stripe API call fails for a tier, the tier is returned with prices: vec![] and the error is logged — the whole request doesn’t fail.
### GET /api/billing/subscription
Returns `Option<OrgSubscriptionInfo>` — `None` if the org hasn't subscribed yet:
```rustpub struct OrgSubscriptionInfo { pub tier_slug: String, pub tier_name: String, pub status: SubscriptionStatus, pub seat_count: i32, pub billing_period_end: String, // ISO-8601 pub included_credits: i32, pub purchased_credits: i32,}