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 → matches to a
subscription_tierrow - 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 on the billing settings page
- Frontend calls
POST /api/billing/checkoutwith the tier slug - Server creates a Stripe Checkout Session via
create_checkout_session() - Returns a
StripeRedirect { url }— the browser navigates to Stripe’s hosted page - Stripe delivers
customer.subscription.createdwebhook → subscription and budget are provisioned
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>:
pub struct SubscriptionTierInfo { pub id: Uuid, pub slug: String, pub name: String, pub price_per_seat_cents: i32, 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,}GET /api/billing/subscription
Section titled “GET /api/billing/subscription”Returns Option<OrgSubscriptionInfo> — None if the org hasn’t subscribed yet:
pub 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,}