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(unlesspromoCodeis provided)billing_address_collection: required- Referral coupon
g0ifr2ep(15% off annual for invited users) subscription_data.metadata.user_idfor webhook propagation- Success URL redirects to
/create
Promo Code Validation¶
{
"valid": true,
"discount": { "type": "percent", "amount": 100 },
"promotion_code_id": "promo_..."
}
Usage Tracking¶
{
"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 countscustomer.subscription.updated— Update plan_key and statuscustomer.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 |