Stripe Billing
Corral wires your corral.yaml plans directly to Stripe.
One command syncs products, prices, and webhooks. You get checkout sessions,
billing portal, and subscription lifecycle โ all handled.
corral stripe sync (alias: corral stripe push) to sync products/prices,
register webhooks, and power checkout + billing portal endpoints.
Corral supports usage metering and free trials out of the box.
Plan Configuration
Define your plans in corral.yaml. Every field maps directly
to a Stripe Product + Price object when you run corral stripe push.
plans:
# Free plan โ no Stripe product created
free:
price: 0
description: Basic access. No credit card required.
features: [basic_search, export_csv]
# Paid monthly plan
pro:
price: 49
interval: month
description: Full analysis suite for HORIZON analysts.
trial_days: 14
stripe_metadata:
tier: pro
seat_type: analyst
# Annual plan (same features as pro, discounted)
pro_annual:
price: 490
interval: year
description: Pro plan, billed annually (2 months free).
inherits: pro # shares all pro features
# Custom enterprise โ no Stripe price (manual invoicing)
enterprise:
price: custom
description: Air-gapped deployment, custom SLA, SSO.
contact: enterprise@horizon.mil
corral stripe push
Syncs your YAML plans to Stripe. Creates Products and Prices for any plan with a numeric price. If products already exist (matched by metadata), updates them. Idempotent.
corral stripe push --env production to push to live mode.
Always run --dry-run first to preview changes:
corral stripe push --dry-run --env production.
Checkout Flow โ Web
Trigger a Stripe Checkout session from your frontend. Corral's billing endpoint handles session creation and redirect.
Option A โ Link to checkout URL
{/* Simplest: link directly to Corral's checkout endpoint */}
<a href="/api/billing/checkout?plan=pro&return_url=/dashboard">
Upgrade to Pro
</a>
Option B โ Server action (Next.js)
'use server'
import { corral } from '@/lib/corral'
import { redirect } from 'next/navigation'
export async function startCheckout(plan: 'pro' | 'pro_annual') {
const session = await getSession()
if (!session) redirect('/login')
const { url } = await corral.billing.createCheckoutSession({
userId: session.user.id,
plan,
successUrl: `${process.env.NEXT_PUBLIC_URL}/dashboard?upgraded=1`,
cancelUrl: `${process.env.NEXT_PUBLIC_URL}/pricing`,
})
redirect(url)
}
The checkout sequence
- User clicks "Upgrade to Pro"
- Your server calls
corral.billing.createCheckoutSession()(or hitsPOST /api/billing/checkout) - Corral creates a Stripe Checkout Session and returns its URL
- User is redirected to
checkout.stripe.com - User completes payment
- Stripe fires
checkout.session.completedwebhook - Corral's webhook handler updates the user's plan in your database
- User is redirected to your
successUrl - On next request, their session reflects the new plan
Checkout Flow โ CLI / Device Auth
When a CLI tool needs to upgrade the user's plan, it can't open a browser directly. Corral supports a CLI-friendly checkout flow using device auth.
Webhooks
Corral auto-registers and handles Stripe webhooks at
/api/billing/webhook. The core lifecycle events are
handled automatically โ you hook in for custom logic.
Built-in webhook handling
| Stripe Event | Corral Action |
|---|---|
checkout.session.completed | Activate subscription, update user plan, send welcome email |
customer.subscription.updated | Update plan in DB, handle upgrades/downgrades |
customer.subscription.deleted | Downgrade to free plan, send cancellation email |
invoice.payment_succeeded | Reset usage meters for new billing period |
invoice.payment_failed | Mark subscription as past_due, send payment failure email |
customer.subscription.trial_will_end | Send 3-day trial ending email |
Custom webhook handlers
Add your own logic for any Stripe event in corral.yaml:
webhooks:
invoice.payment_failed:
handler: src/handlers/payment-failed.ts
customer.subscription.updated:
handler: src/handlers/subscription-updated.ts
import type { WebhookHandler } from '@corral/core'
import { sendSlackAlert } from '@/lib/slack'
export const handler: WebhookHandler<'invoice.payment_failed'> = async ({
event,
user,
subscription,
}) => {
// event.data.object is the Stripe Invoice
const invoice = event.data.object
// Send alert to ops team
await sendSlackAlert({
channel: '#billing-alerts',
text: `Payment failed for ${user.email}: $${invoice.amount_due / 100}`,
})
}
export default handler
Billing Portal
Corral provides a pre-built billing portal link that takes users to Stripe's hosted Customer Portal โ where they can update payment methods, download invoices, or cancel their subscription.
{/* Simple link โ Corral creates the portal session server-side */}
<a href="/api/billing/portal?return_url=/dashboard">
Manage Billing
</a>
{/* Or in a server action: */}
const { url } = await corral.billing.createPortalSession({
userId: session.user.id,
returnUrl: 'https://horizon.mil/dashboard',
})
redirect(url)
Customizing the portal
Configure the Stripe Customer Portal in your Stripe Dashboard under Billing โ Customer Portal. You can control which features are available (cancellation, plan switching, payment method updates).
Testing with Stripe Test Mode
Corral automatically uses test mode when STRIPE_SECRET_KEY
starts with sk_test_. Use Stripe's test card numbers:
| Card Number | Behavior |
|---|---|
4242 4242 4242 4242 | Successful payment |
4000 0025 0000 3155 | Requires 3D Secure authentication |
4000 0000 0000 9995 | Payment declined (insufficient funds) |
4000 0000 0000 0069 | Card expired |
Automated billing test
Run a full checkout + webhook cycle in CI without a browser:
Annual Plans
Offer annual pricing alongside monthly. Corral handles the plan
mapping so that pro and pro_annual users
both get the same feature set.
plans:
pro:
price: 49
interval: month
pro_annual:
price: 490
interval: year
inherits: pro # same features as pro
display_name: Pro (Annual)
features:
ai_analysis:
# Both pro and pro_annual get this feature
plans: [pro, pro_annual, enterprise]
Metered / Usage-Based Billing
For plans with overage charges, Corral integrates with Stripe's
metered billing. Set overage: charge on a meter and
Corral will report usage to Stripe at the end of each billing period.
meters:
api_calls:
plans:
free: 500
pro: 10000
window: month
overage: charge
overage_price: 0.001 # $0.001 per API call over limit
stripe_meter_id: env:STRIPE_METER_API_CALLS
corral stripe push
and Corral will prompt you through the setup, or do it manually in the
Stripe dashboard.