Postiz uses Stripe for subscription billing, payment processing, and revenue management. Configure Stripe to enable paid plans and billing features.
Features
- Subscription management (Standard and Pro plans)
- Monthly and yearly billing cycles
- 7-day free trial support
- Proration for plan changes
- Promotion codes and coupons
- Billing portal for customers
- Webhook event processing
- Refund management
- Usage tracking and analytics
Environment Variables
Add these variables to your .env file:
# Stripe Keys
STRIPE_PUBLISHABLE_KEY="pk_live_xxxxxxxxxxxxxxxxxxxx"
STRIPE_SECRET_KEY="sk_live_xxxxxxxxxxxxxxxxxxxx"
# Webhook Signing Secrets
STRIPE_SIGNING_KEY="whsec_xxxxxxxxxxxxxxxxxxxx"
STRIPE_SIGNING_KEY_CONNECT="whsec_xxxxxxxxxxxxxxxxxxxx"
# Fee Configuration
FEE_AMOUNT=0.05 # 5% platform fee
If Stripe keys are not configured, billing features will be disabled but Postiz will continue to function normally.
Setup Guide
Create Stripe Account
- Visit stripe.com and create an account
- Complete business verification
- Navigate to the Dashboard
Get API Keys
- Go to Developers → API keys
- Reveal and copy:
- Publishable key (starts with
pk_)
- Secret key (starts with
sk_)
Use test keys (pk_test_ and sk_test_) for development. Switch to live keys for production.
Create Products
Create pricing products for your subscription plans:
- Go to Products → Add product
- Create two products:
- Standard - Basic plan
- Pro - Professional plan
- For each product, create two prices:
- Monthly recurring
- Yearly recurring
Configure Webhooks
Set up webhook endpoints to receive Stripe events:
- Go to Developers → Webhooks
- Click “Add endpoint”
- Enter webhook URL:
https://your-domain.com/api/billing/stripe/webhook
- Select events to listen for:
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_succeeded
- Copy the signing secret (starts with
whsec_)
Configure Environment
Add to your .env file:STRIPE_PUBLISHABLE_KEY="pk_live_xxxxxxxxxxxxxxxxxxxx"
STRIPE_SECRET_KEY="sk_live_xxxxxxxxxxxxxxxxxxxx"
STRIPE_SIGNING_KEY="whsec_xxxxxxxxxxxxxxxxxxxx"
STRIPE_SIGNING_KEY_CONNECT="whsec_xxxxxxxxxxxxxxxxxxxx"
FEE_AMOUNT=0.05
Restart Postiz
Apply the configuration:docker compose restart backend
Subscription Flow
Postiz implements a complete subscription management system:
Creating Subscriptions
From stripe.service.ts:718-824:
async subscribe(
uniqueId: string,
organizationId: string,
userId: string,
body: BillingSubscribeDto,
allowTrial: boolean
) {
const org = await this._organizationService.getOrgById(organizationId);
const customer = await this.createOrGetCustomer(org!);
// Find or create Stripe product and price
const findProduct = await stripe.products.create({
active: true,
name: body.billing,
});
const findPrice = await stripe.prices.create({
active: true,
product: findProduct.id,
currency: 'usd',
unit_amount: priceData.month_price * 100,
recurring: {
interval: body.period === 'MONTHLY' ? 'month' : 'year',
},
});
// Create checkout session
return this.createCheckoutSession(
uniqueId,
id,
customer,
body,
findPrice.id,
userId,
allowTrial
);
}
Embedded Checkout
Postiz supports Stripe’s embedded checkout for seamless integration:
// From stripe.service.ts:424-495
const { client_secret } = await stripe.checkout.sessions.create({
ui_mode: 'custom',
customer,
return_url: process.env['FRONTEND_URL'] + `/launches?onboarding=true&check=${uniqueId}`,
mode: 'subscription',
subscription_data: {
...(allowTrial ? { trial_period_days: 7 } : {}),
metadata: {
service: 'gitroom',
...body,
userId,
uniqueId,
},
},
allow_promotion_codes: body.period === 'MONTHLY',
line_items: [{ price, quantity: 1 }],
});
return { client_secret, auto_apply_coupon };
Webhook Event Handling
Subscription Created
From stripe.service.ts:102-131:
async createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) {
const { uniqueId, billing, period } = event.data.object.metadata;
// Validate card for trial subscriptions
const check = await this.checkValidCard(event);
if (!check) {
return { ok: false };
}
return this._subscriptionService.createOrUpdateSubscription(
event.data.object.status !== 'active',
uniqueId,
event.data.object.customer as string,
pricing[billing].channel!,
billing,
period,
event.data.object.cancel_at
);
}
Subscription Updated
// From stripe.service.ts:132-157
async updateSubscription(event: Stripe.CustomerSubscriptionUpdatedEvent) {
const { uniqueId, billing, period } = event.data.object.metadata;
const check = await this.checkValidCard(event);
if (!check) {
return { ok: false };
}
return this._subscriptionService.createOrUpdateSubscription(
event.data.object.status !== 'active',
uniqueId,
event.data.object.customer as string,
pricing[billing].channel!,
billing,
period,
event.data.object.cancel_at
);
}
Payment Succeeded
// From stripe.service.ts:826-846
async paymentSucceeded(event: Stripe.InvoicePaymentSucceededEvent) {
const subscriptionId = event.data.object.parent?.subscription_details?.subscription;
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const { userId, ud } = subscription.metadata;
const user = await this._userService.getUserById(userId);
// Track purchase event for analytics
if (user && user.ip && user.agent) {
this._trackService.track(ud, user.ip, user.agent, TrackEnum.Purchase, {
value: event.data.object.amount_paid / 100,
});
}
return { ok: true };
}
Free Trial Implementation
Card Validation for Trials
From stripe.service.ts:28-100:
async checkValidCard(event: Stripe.CustomerSubscriptionCreatedEvent) {
const org = await this._organizationService.getOrgByCustomerId(
event.data.object.customer as string
);
if (!org?.allowTrial) {
return true;
}
// Get latest payment method
const paymentMethods = await stripe.paymentMethods.list({
customer: event.data.object.customer as string,
});
const latestMethod = paymentMethods.data.reduce((prev, current) => {
if (prev.created < current.created) {
return current;
}
return prev;
});
// Verify card with $1 authorization (then cancel)
const paymentIntent = await stripe.paymentIntents.create({
amount: 100,
currency: 'usd',
payment_method: latestMethod.id,
customer: event.data.object.customer as string,
automatic_payment_methods: {
allow_redirects: 'never',
enabled: true,
},
capture_method: 'manual',
confirm: true,
});
if (paymentIntent.status !== 'requires_capture') {
await stripe.subscriptions.cancel(event.data.object.id);
return false;
}
await stripe.paymentIntents.cancel(paymentIntent.id);
return true;
}
Postiz validates trial subscriptions with a $1 authorization that is immediately cancelled to prevent abuse.
Proration
Automatic proration when users change plans:
// From stripe.service.ts:206-290
async prorate(organizationId: string, body: BillingSubscribeDto) {
const org = await this._organizationService.getOrgById(organizationId);
const customer = await this.createOrGetCustomer(org!);
const proration_date = Math.floor(Date.now() / 1000);
const currentSubscription = await stripe.subscriptions.list({ customer });
const price = await stripe.invoices.createPreview({
customer,
subscription: currentSubscription.data[0]?.id,
subscription_details: {
proration_behavior: 'create_prorations',
billing_cycle_anchor: 'now',
items: [
{
id: currentSubscription.data[0]?.items?.data?.[0]?.id,
price: findPrice.id!,
quantity: 1,
},
],
proration_date: proration_date,
},
});
return {
price: price?.amount_remaining ? price.amount_remaining / 100 : 0,
};
}
From stripe.service.ts:372-422:
private async findAutoApplyPromotionCode(): Promise<string | null> {
const promotionCodes = await stripe.promotionCodes.list({
active: true,
limit: 100,
});
const now = Math.floor(Date.now() / 1000);
for (const promoCode of promotionCodes.data) {
const coupon = promoCode.promotion.coupon;
// Check if autoapply metadata is set
const autoApply = Object.assign(
{},
promoCode.metadata,
coupon?.metadata
)?.autoapply;
if (autoApply !== 'true') continue;
// Check expiration
if (promoCode.expires_at && promoCode.expires_at < now) continue;
if (coupon?.redeem_by && coupon.redeem_by < now) continue;
// Check max redemptions
if (
promoCode.max_redemptions &&
promoCode.times_redeemed >= promoCode.max_redemptions
) continue;
return promoCode.code;
}
return null;
}
Set autoapply: true in promotion code metadata to automatically apply it to monthly subscriptions.
Billing Portal
Customers can manage their subscriptions through Stripe’s billing portal:
// From stripe.service.ts:365-370
async createBillingPortalLink(customer: string) {
return stripe.billingPortal.sessions.create({
customer,
return_url: process.env['FRONTEND_URL'] + '/billing',
});
}
Features:
- Update payment method
- View invoices
- Download receipts
- Cancel subscription
Subscription Cancellation
Cancel at Period End
From stripe.service.ts:301-358:
async setToCancel(organizationId: string) {
const org = await this._organizationService.getOrgById(organizationId);
const customer = await this.createOrGetCustomer(org!);
const subscriptions = await stripe.subscriptions.list({
customer,
status: 'all',
expand: ['data.latest_invoice'],
});
const sub = subscriptions.data.filter(f => f.status !== 'canceled')[0];
// Toggle cancellation
if (sub.cancel_at_period_end) {
const { cancel_at } = await stripe.subscriptions.update(sub.id, {
cancel_at_period_end: false,
});
return {
id: makeId(10),
cancel_at: cancel_at ? new Date(cancel_at * 1000) : undefined,
};
}
// Check if payment failed
const latestInvoice = sub.latest_invoice as Stripe.Invoice | null;
const hasFailedPayment =
sub.status === 'past_due' ||
latestInvoice?.status === 'open' ||
latestInvoice?.status === 'uncollectible';
if (hasFailedPayment) {
// Cancel immediately
await stripe.subscriptions.cancel(sub.id);
await this._subscriptionService.deleteSubscription(customer);
return { id: makeId(10), cancel_at: new Date() };
}
// Cancel at period end
const { cancel_at } = await stripe.subscriptions.update(sub.id, {
cancel_at_period_end: true,
});
return {
id: makeId(10),
cancel_at: cancel_at ? new Date(cancel_at * 1000) : undefined,
};
}
Refunds
Administrators can issue refunds:
// From stripe.service.ts:873-892
async refundCharges(organizationId: string, chargeIds: string[]) {
const org = await this._organizationService.getOrgById(organizationId);
const refunded: string[] = [];
const failed: string[] = [];
for (const chargeId of chargeIds) {
try {
await stripe.refunds.create({ charge: chargeId });
refunded.push(chargeId);
} catch (err) {
failed.push(chargeId);
}
}
return { refunded, failed };
}
Testing
Test Mode
Use Stripe test keys for development:
STRIPE_PUBLISHABLE_KEY="pk_test_xxxxxxxxxxxxxxxxxxxx"
STRIPE_SECRET_KEY="sk_test_xxxxxxxxxxxxxxxxxxxx"
Test Cards
Stripe provides test card numbers:
- Success:
4242 4242 4242 4242
- Declined:
4000 0000 0000 0002
- Requires Authentication:
4000 0027 6000 3184
Testing Webhooks Locally
Use Stripe CLI to forward webhook events:
stripe listen --forward-to localhost:3000/api/billing/stripe/webhook
This provides a test webhook signing secret to use in development.
Troubleshooting
Webhook signature verification failed
If webhooks fail with signature errors:
- Verify
STRIPE_SIGNING_KEY matches the webhook secret in Stripe dashboard
- Ensure raw request body is used (not parsed JSON)
- Check webhook endpoint is publicly accessible
- Verify HTTPS is used in production
Test webhook:stripe trigger customer.subscription.created
Trial card validation failing
If trial subscriptions are being cancelled:
- Verify payment method is valid
- Check card has sufficient funds for $1 authorization
- Ensure 3D Secure is handled for cards that require it
- Review logs for specific error messages
Disable card validation for testing:
Proration calculations incorrect
Ensure:
- Subscription metadata includes correct pricing tier
proration_behavior is set to 'create_prorations'
- Billing cycle anchor is configured correctly
- Current subscription is active
Customer not found errors
If you see “customer not found”:
- Verify organization has
paymentId set
- Check customer exists in Stripe dashboard
- Ensure using correct Stripe account (test vs live)
- Re-create customer if needed
Promotion codes not applying
Security Best Practices
Never expose Stripe secret keys in client-side code or commit them to version control.
- Store all Stripe keys in environment variables
- Use test keys for development, live keys only in production
- Verify webhook signatures for all incoming events
- Use HTTPS for all webhook endpoints
- Implement proper access controls for billing operations
- Regularly rotate API keys
- Monitor Stripe dashboard for suspicious activity
- Enable Stripe Radar for fraud detection
Postiz supports platform fees for marketplace scenarios:
This deducts a percentage from each payment for platform revenue.
Next Steps
After configuring Stripe billing: