Skip to content

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.

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.

The handler implements Stripe’s HMAC-SHA256 signature scheme:

  1. Extract t=<timestamp> and v1=<hex> entries from the Stripe-Signature header
  2. Compute HMAC-SHA256(webhook_secret, "{timestamp}.{body}")
  3. Constant-time compare against all v1 signatures
  4. Reject if the timestamp is older than 5 minutes

Verified events are routed by dispatch_stripe_event():

Stripe EventHandler
customer.subscription.createdUpserts organization_subscription + creates org_budget
customer.subscription.updatedUpdates status, seats, tier, and period
customer.subscription.deletedMarks subscription as canceled
invoice.paidResets cycle budget (replenishes included credits)
checkout.session.completedHandles post-checkout setup
All othersLogged and acknowledged (200 OK)

All handlers are idempotent — safe for Stripe to redeliver on transient failures (the server returns 5xx to trigger retries).

On customer.subscription.created/updated, the handler:

  1. Extracts stripe_customer_id → looks up the org
  2. Reads the Stripe Price ID → matches to a subscription_tier row
  3. Inserts or updates organization_subscription with status, seat count, and billing period
  4. On create: initializes org_budget with 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.

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.

  1. The org owner selects a tier on the billing settings page
  2. Frontend calls POST /api/billing/checkout with the tier slug
  3. Server creates a Stripe Checkout Session via create_checkout_session()
  4. Returns a StripeRedirect { url } — the browser navigates to Stripe’s hosted page
  5. Stripe delivers customer.subscription.created webhook → subscription and budget are provisioned

Only organization owners can initiate checkout.

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.

MethodPathAuthDescription
GET/api/billing/tiersPublicList active subscription tiers
GET/api/billing/subscriptionSessionCurrent org’s subscription summary
POST/api/billing/checkoutOwnerCreate Stripe Checkout Session
POST/api/billing/portalOwnerCreate Stripe Billing Portal Session
POST/api/billing/stripe/webhookStripe signatureWebhook receiver

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,
}

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,
}