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.

Billing in v0.4.1
Use 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.

corral.yamlyaml
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 Using STRIPE_SECRET_KEY (test mode: sk_test_...) Created Product: HORIZON Pro โ†’ prod_R2xK9mN2aLp Created Price: $49/month โ†’ price_1Ov9mR2xK9mN Created Product: HORIZON Pro Annual โ†’ prod_R2xK9mN3bMq Created Price: $490/year โ†’ price_1Ov9mR2xK9mQ Skipped Plan: free (no Stripe product needed) Skipped Plan: enterprise (custom pricing) Registered webhook: https://horizon.mil/api/billing/webhook Events: customer.subscription.*, invoice.*, checkout.session.* Writing price IDs to .env.local: STRIPE_PRICE_PRO=price_1Ov9mR2xK9mN STRIPE_PRICE_PRO_ANNUAL=price_1Ov9mR2xK9mQ Done. Run corral validate to confirm sync.
Production push
Use 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

Upgrade buttontsx
{/* 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)

src/app/upgrade/actions.tstypescript
'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

  1. User clicks "Upgrade to Pro"
  2. Your server calls corral.billing.createCheckoutSession() (or hits POST /api/billing/checkout)
  3. Corral creates a Stripe Checkout Session and returns its URL
  4. User is redirected to checkout.stripe.com
  5. User completes payment
  6. Stripe fires checkout.session.completed webhook
  7. Corral's webhook handler updates the user's plan in your database
  8. User is redirected to your successUrl
  9. 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.

horizon-cli upgrade
horizon upgrade --plan pro To upgrade to HORIZON Pro ($49/month): Open: https://horizon.mil/upgrade?token=tok_Xk9m2N Code: XKCD-9821 Waiting for payment confirmation... Payment confirmed! You're now on HORIZON Pro. API call limit: 10,000/month

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 EventCorral Action
checkout.session.completedActivate subscription, update user plan, send welcome email
customer.subscription.updatedUpdate plan in DB, handle upgrades/downgrades
customer.subscription.deletedDowngrade to free plan, send cancellation email
invoice.payment_succeededReset usage meters for new billing period
invoice.payment_failedMark subscription as past_due, send payment failure email
customer.subscription.trial_will_endSend 3-day trial ending email

Custom webhook handlers

Add your own logic for any Stripe event in corral.yaml:

corral.yamlyaml
webhooks:
  invoice.payment_failed:
    handler: src/handlers/payment-failed.ts
  customer.subscription.updated:
    handler: src/handlers/subscription-updated.ts
src/handlers/payment-failed.tstypescript
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.

tsx
{/* 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).

Plan switching in the portal
If you enable plan switching in the Stripe Customer Portal, Corral's webhook handler automatically updates the user's plan when they switch. No extra code needed.

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 NumberBehavior
4242 4242 4242 4242Successful payment
4000 0025 0000 3155Requires 3D Secure authentication
4000 0000 0000 9995Payment declined (insufficient funds)
4000 0000 0000 0069Card expired

Automated billing test

Run a full checkout + webhook cycle in CI without a browser:

corral stripe test --plan pro --email analyst@horizon-test.mil Creating test customer... Triggering checkout.session.completed webhook... Waiting for webhook delivery... User plan updated: free โ†’ pro Triggering invoice.payment_succeeded... Meters reset for new billing period Triggering customer.subscription.deleted... User plan updated: pro โ†’ free All billing lifecycle events handled correctly โœ“

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.

corral.yamlyaml
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.

corral.yamlyaml
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
Metered billing setup
Metered billing requires creating a Stripe Meter in your dashboard and adding a metered price to your product. Run corral stripe push and Corral will prompt you through the setup, or do it manually in the Stripe dashboard.