Skip to content

Billing & Subscriptions

MDU uses Stripe for all payments: subscriptions, one-time purchases, and the referral reward system.

Subscription Plans

Plan Generations/mo STL Exports/mo Price
Free 1 10 Free
Starter 10 30 Annual billing
Pro 30 50 Annual billing

Plan limits are enforced by plans.cjs and checked via checkGenerationLimit() before every AI call.

Note

VAT is included in all prices (tax-inclusive). automatic_tax is disabled in Stripe. Manual VAT declaration to accountant.

Endpoints

Subscriptions

Method Path Auth Description
POST /api/create-checkout Required Create Stripe checkout session
POST /api/customer-portal Required Stripe billing portal
POST /api/check-subscription Required Check active subscription
GET /api/usage Required Current month usage stats
POST /api/validate-promo - Validate promo code pre-checkout

One-Time Purchases

Method Path Auth Description
POST /api/texture-checkout Required Texture payment (4.50)
POST /api/private-model-checkout Required Privacy payment (4.00)
POST /api/delete-model-checkout Required Deletion payment (3.50)
POST /api/private-map-checkout Required Map privacy (4.00)
POST /api/delete-map-checkout Required Map deletion (3.50)

Confirmation (post-payment)

Method Path Auth Description
POST /api/confirm-texture-payment Required Mark texture_paid
POST /api/confirm-privacy-payment Required Set privacy_paid + is_public=false
POST /api/confirm-model-deletion Required DELETE model record
POST /api/confirm-map-privacy Required Set map privacy_paid
POST /api/confirm-map-deletion Required DELETE map record
POST /api/update-model-privacy Required Toggle is_public
POST /api/update-map-privacy Required Toggle map is_public

Checkout Flow

1. POST /api/create-checkout { priceId, promoCode? }
   → Check referral invitation → apply 15% coupon if annual
   → stripe.checkout.sessions.create()
   → Return { url }

2. User completes Stripe Checkout

3. Webhook: checkout.session.completed
   → Activate subscription in local DB

4. POST /api/check-subscription
   → Return { subscribed, plan_key, rewards }

Create Checkout

POST /api/create-checkout
Authorization: Bearer <token>

{ "priceId": "price_1T3zzWLJD1tIg78QNNrfWUnf" }

Features:

  • allow_promotion_codes: true (unless promoCode is provided)
  • billing_address_collection: required
  • Referral coupon g0ifr2ep (15% off annual for invited users)
  • subscription_data.metadata.user_id for webhook propagation
  • Success URL redirects to /create

Promo Code Validation

POST /api/validate-promo

{ "code": "MDU100" }
{
  "valid": true,
  "discount": { "type": "percent", "amount": 100 },
  "promotion_code_id": "promo_..."
}

Usage Tracking

GET /api/usage
Authorization: Bearer <token>
{
  "plan": "pro",
  "plan_name": "Pro",
  "usage": {
    "generations": { "used": 5, "limit": 30, "remaining": 25 },
    "stl_exports": { "used": 3, "limit": 50, "remaining": 47 }
  },
  "period_start": "2026-03-01T00:00:00.000Z"
}

One-Time Payment Flow

All one-time payments (texture, privacy, deletion) follow the same pattern:

1. POST /api/{action}-checkout { modelId }
   → Verify ownership (WHERE id=$id AND user_id=$userId)
   → Create Stripe checkout session with metadata
   → Return { url }

2. User completes payment

3. POST /api/confirm-{action} { modelId }
   → Verify ownership again
   → Find matching paid session in Stripe
   → Apply DB change
   → Return { success: true }

Privacy Toggle

Making a model/map private requires either: - Active subscription, OR - privacy_paid = true (one-time purchase)

Making public is always free.

Webhooks

MDU has two Stripe webhook endpoints:

Path Handler Events
/api/webhooks/stripe 3dplim (Next.js) 3dplim subscription events
/api/webhooks/mdu-stripe mdu-api Miniature Forge events

mdu-api Webhook Events

  • checkout.session.completed — Activate subscription, record counts
  • customer.subscription.updated — Update plan_key and status
  • customer.subscription.deleted — Downgrade to free

Security: raw body parsing + stripe.webhooks.constructEvent() signature verification + event dedup via stripe_webhook_events table.

Referral System

Endpoints

Method Path Auth Description
GET /api/referral-stats Required Referral stats + bonuses
POST /api/apply-referral-rewards Required Compute milestone bonuses
POST /api/send-invite Required Send referral email

Reward Milestones

Every N subscribed referrals grants +10 bonus days:

  • Annual plans: N = 10
  • Monthly plans: N = 30

Implementation: computeAndApplyRewards() extends stripe.subscriptions.trial_end.

Referral Stats

{
  "invite_code": "ABC123",
  "invite_url": "https://app.minidreamuniverse.com?invite=ABC123",
  "total_invited": 5,
  "total_converted": 3,
  "bonus_generations": 30,
  "bonus_stl_exports": 30
}

Subscriptions Table

Column Type Description
user_id UUID Primary key
plan_key TEXT free, starter, pro
stripe_subscription_id TEXT Stripe sub ID
status TEXT active, canceled, etc.
current_period_end TIMESTAMP Billing period end