Payment configuration
TinyShip payment system configuration guide
Payments are a core part of TinyShip. For a global product, start with Stripe, PayPal, Creem, or Dodo Payments. WeChat Pay and Alipay are optional and only for buyers in mainland China. Three billing modes are supported: one-time payment, subscription, and credit packs. WeChat Pay and Alipay support one-time payments and credit packs only.
Credits: To configure AI credit consumption, see the credits guide.
AI setup: If you have Agent Skills installed, you can ask your AI to "help me configure payment". The tinyship-payment skill walks through the setup.
Pricing mode: static vs dynamic
Choose a pricing mode before you add plans:
- Static pricing (default): Plans live in
config/payment.tsand ship with the code. Use this when plans rarely change. The "Configure plans" section below is the static path. - Dynamic pricing: Plans live in the database. Create and edit them at
/admin/pricingwithout a redeploy. This also adds strikethrough prices, locale filtering, and custom ordering. See Dynamic pricing.
Switch modes with the PRICING_MODE environment variable. The two modes do not overwrite each other.
Related pages
| Page | Path | Description |
|---|---|---|
| Pricing | /pricing | Lists every plan |
| Payment success | /payment-success | Return page after a successful payment |
| Payment cancel | /payment-cancel | Return page after a cancelled payment |
| Dashboard | /dashboard | Subscription status and order history |
Supported providers
| Provider | One-time | Subscription | Credits | Market | Currency |
|---|---|---|---|---|---|
| Stripe | Yes | Yes | Yes | Global | Many currencies |
| PayPal | Yes | Yes | Yes | Global | Many currencies |
| Creem | Yes | Yes | Yes | Global | USD, EUR, and others |
| Dodo Payments | Yes | Yes | Yes | Global | Many currencies |
| WeChat Pay | Yes | No | Yes | Mainland China (optional) | CNY |
| Alipay | Yes | No | Yes | Mainland China (optional) | CNY |
Configuration overview
Set providers in config/payment.ts. Enable the ones you will use.
Global
- Stripe — cards, one-time payments, and subscriptions
- PayPal — one-time payments and subscriptions
- Creem — indie-friendly checkout, with a simpler review than Stripe
- Dodo Payments — Merchant of Record that handles tax and compliance
China (optional)
- WeChat Pay and Alipay — for customers in China. Both require a Chinese business entity and a merchant account.
Local testing: Prefer each provider's test or sandbox mode. Stripe, Creem, PayPal, Alipay, and Dodo Payments all have one. WeChat Pay does not. To test WeChat Pay, place a real charge of 0.01 CNY. Plan amounts for WeChat Pay are yuan. The provider multiplies by 100 before it calls WeChat.
Provider guides
Cards, subscriptions, and one-time payments.
Global checkout. Subscriptions and one-time payments.
Indie-friendly checkout with a lighter review than Stripe.
Merchant of Record. Tax and compliance are handled for you.
China (optional)
Customers in China. QR checkout. A Chinese business entity is required.
Customers in China. A Chinese business entity is required.
Store plans in the database and edit them without a redeploy.
Local test mode and webhook debugging.
Configure plans
Plans live on plans in config/payment.ts. They show up on /pricing.
Dynamic pricing: To change plans without a redeploy, use dynamic pricing. Plans are then read from the database and edited at /admin/pricing. You do not need to edit config/payment.ts.
Plan types
| Type | What it does | Typical use |
|---|---|---|
| One-time | One payment for long-term or permanent access | Lifetime membership, a software license, a course |
| Subscription (recurring) | Bills on a schedule | SaaS, memberships |
| Credits | A pack of credits, spent as the product is used | AI chat, image generation |
Plan shape
Each plan is a typed object. Provider-specific IDs sit on that object (stripePriceId, paypalPlanId, creemProductId, dodoProductId).
plans: {
lifetime: {
provider: 'stripe', // 'stripe' | 'dodo' | 'creem' | 'paypal' | 'wechat' | 'alipay'
id: 'lifetime', // Unique plan id
amount: 999, // Price. WeChat Pay and Alipay amounts are yuan.
currency: 'USD', // 'USD' | 'EUR' | 'CNY' | …
recommended: true, // Highlight on the pricing page
duration: {
months: 999999, // 999999 means lifetime (months >= 9999)
type: 'one_time' // 'one_time' | 'recurring' | 'credits'
},
stripePriceId: 'price_xxx', // Stripe Price ID
paypalPlanId: 'P-xxx', // PayPal Plan ID (subscriptions)
creemProductId: 'prod_xxx', // Creem product ID
dodoProductId: 'pdt_xxx', // Dodo product ID
i18n: { /* locale copy */ }
}
}Set only the provider ID that matches provider. A Stripe plan needs stripePriceId. A PayPal subscription needs paypalPlanId. A Creem plan needs creemProductId. A Dodo plan needs dodoProductId. WeChat Pay and Alipay do not use a product ID on the plan.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Which provider charges this plan |
id | string | Yes | Unique plan id, used by the API |
amount | number | Yes | Price. WeChat Pay and Alipay amounts are yuan. For Stripe, the charge comes from stripePriceId. |
currency | string | Yes | ISO 4217 currency code |
recommended | boolean | No | Highlight on the pricing page |
duration.months | number | One-time and recurring | Length in months. 9999 or more is lifetime. Omit for credit packs. |
duration.type | string | Yes | one_time, recurring, or credits |
credits | number | Credit packs | Credits granted after purchase |
stripePriceId | string | Stripe | Stripe Price ID |
paypalPlanId | string | PayPal subscriptions | PayPal Plan ID |
creemProductId | string | Creem | Creem product ID |
dodoProductId | string | Dodo Payments | Dodo product ID |
Locale copy
i18n: {
'en': {
name: 'Monthly Plan',
description: 'Perfect for short-term projects',
duration: 'month',
features: ['All premium features', 'Priority support']
}
}Full example (USD, Stripe)
lifetime: {
provider: 'stripe',
id: 'lifetime',
amount: 999,
currency: 'USD',
recommended: true,
duration: {
months: 999999,
type: 'one_time'
},
stripePriceId: 'price_1RL2IcDjHLfDWeHDMCmobkzb',
i18n: {
'en': {
name: 'TinyShip Complete',
description: 'Lifetime access to the complete SaaS starter kit',
duration: 'lifetime',
features: [
'Complete SaaS template source code',
'Free lifetime updates',
'Priority customer support',
'Access to a private GitHub repository'
]
}
}
}Strikethrough prices use originalPrice on dynamic plans (pricing_plan), not a field in config/payment.ts.
Local testing: WeChat Pay and Alipay charge amount directly. Use 0.01 yuan while testing those providers. Stripe bills the Price in the Dashboard (stripePriceId). See Payment testing.
Payment flow
- Buyer selects a plan → 2. Order is created → 3. Redirect to the provider → 4. Webhook is handled → 5. Order status is updated
The factory in libs/payment/index.ts is createPaymentProvider('stripe' | 'wechat' | 'creem' | 'dodo').
API routes
// Start checkout
POST /api/payment/initiate
{
"planId": "monthly",
"provider": "stripe"
}
// Order status (query string, not a path segment)
GET /api/payment/query?orderId=<orderId>&provider=<provider>
// Webhooks
POST /api/payment/webhook/stripe
POST /api/payment/webhook/paypal
POST /api/payment/webhook/creem
POST /api/payment/webhook/dodo
POST /api/payment/webhook/wechat
POST /api/payment/webhook/alipay
// Cancel (query string). Implemented for WeChat Pay.
POST /api/payment/cancel?orderId=<orderId>&provider=<provider>References
- Payment testing — local development and webhooks
- Credits — credit packs and consumption