اسٹرائپ بلنگ کا نفاذ: سبسکرپشنز، انوائسز اور ویب ہکس
اسٹرائپ بلنگ کے بنیادی ڈھانچے کے لیے اصل معیار ہے، لیکن اسے درست طریقے سے لاگو کرنا — ویب ہکس کو قابلِ توجہ طریقے سے ہینڈل کرنا، سبسکرپشن اسٹیٹ ٹرانزیشن کا انتظام کرنا، اور ناکام ادائیگیوں سے بازیافت کرنا — شروع ہونے والے دستاویزات کے مشورے سے کافی زیادہ خیال رکھتا ہے۔ ایک ادائیگی کا نظام جو 99% وقت کام کرتا ہے اور خاموشی سے ناکام رہتا ہے باقی 1% اس سے بدتر ہے جو 95% پر زور سے ناکام ہو جاتا ہے۔
اس گائیڈ میں 6 ویب ہک ایونٹ کی اقسام، سبسکرپشن لائف سائیکل مینجمنٹ، خودکار لائسنس کی فراہمی، اور ریفنڈ ہینڈلنگ کے ساتھ پروڈکشن اسٹرائپ کے نفاذ کا احاطہ کیا گیا ہے۔ کوڈ کی مثالیں NestJS 11 کا استعمال کرتی ہیں، لیکن پیٹرن کسی بھی بیک اینڈ فریم ورک پر لاگو ہوتے ہیں۔
اہم ٹیک ویز
- چیک آؤٹ ری ڈائریکٹ کی بنیاد پر رسائی کی فراہمی کبھی نہ کریں — ہمیشہ
checkout.session.completedویب ہک کا انتظار کریں۔- کسی بھی ایونٹ پر کارروائی کرنے سے پہلے
stripe.webhooks.constructEvent()کے ساتھ ویب ہک کے دستخطوں کی تصدیق کریں۔- تمام ویب ہُک ہینڈلرز کو غیرمعمولی بنائیں — 72 گھنٹے تک ویب ہُکس کی ناکام کوششیں
- اپنے ڈیٹا بیس میں اسٹرائپ کسٹمر آئی ڈیز، سبسکرپشن آئی ڈیز، اور قیمت آئی ڈیز کو اسٹور کریں - ہر چیز کے لیے اسٹرائپ سے دوبارہ استفسار نہ کریں۔
- ڈننگ ای میلز اور سبسکرپشن معطلی کو متحرک کرنے کے لیے
invoice.payment_failedکو ہینڈل کریں۔charge.refundedاورcheckout.session.completedایک ساتھ مکمل خریداری کے لائف سائیکل کا احاطہ کرتے ہیں۔- اسٹرائپ کی
metadataفیلڈ کا استعمال اپنے اندرونی ریکارڈ سے اسٹرائپ اشیاء کو لنک کرنے کے لیے کریں۔- کبھی بھی خام پٹی والے ویب ہک پے لوڈز کو لاگ ان نہ کریں - ان میں کارڈ کا حساس ڈیٹا ہوتا ہے۔
پروجیکٹ سیٹ اپ
pnpm add stripe
// billing/stripe.client.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
typescript: true,
});
ان ماحولیاتی متغیرات کو ذخیرہ کریں:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
چیک آؤٹ سیشن کی تخلیق
چیک آؤٹ فلو: آپ کا بیک اینڈ اسٹرائپ چیک آؤٹ سیشن بناتا ہے، فرنٹ اینڈ اسٹرائپ پر ری ڈائریکٹ ہوتا ہے، اسٹرائپ ادائیگی جمع کرتا ہے، پھر آپ کے ویب ہک کو کال کرتا ہے۔ کبھی بھی ادائیگی خود نہ کریں — ہمیشہ اسٹرائپ چیک آؤٹ پر ری ڈائریکٹ کریں۔
// billing/billing.service.ts
import Stripe from 'stripe';
import { Injectable, BadRequestException } from '@nestjs/common';
import { stripe } from './stripe.client';
@Injectable()
export class BillingService {
async createCheckoutSession(
userId: string,
orgId: string,
priceId: string,
productId: string
) {
// Get or create Stripe customer
const customer = await this.getOrCreateCustomer(userId, orgId);
const session = await stripe.checkout.sessions.create({
customer: customer.stripeCustomerId,
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: 'payment', // or 'subscription' for recurring
success_url: `${process.env.FRONTEND_URL}/dashboard/billing?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.FRONTEND_URL}/services`,
metadata: {
userId,
organizationId: orgId,
productId,
},
allow_promotion_codes: true,
invoice_creation: {
enabled: true,
},
});
return { url: session.url };
}
async createSubscriptionCheckout(
userId: string,
orgId: string,
priceId: string,
productId: string
) {
const customer = await this.getOrCreateCustomer(userId, orgId);
const session = await stripe.checkout.sessions.create({
customer: customer.stripeCustomerId,
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
subscription_data: {
metadata: {
userId,
organizationId: orgId,
productId,
},
trial_period_days: 14,
},
success_url: `${process.env.FRONTEND_URL}/dashboard/billing?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.FRONTEND_URL}/pricing`,
metadata: {
userId,
organizationId: orgId,
productId,
},
});
return { url: session.url };
}
private async getOrCreateCustomer(userId: string, orgId: string) {
// Check if customer already exists in your DB
const existing = await db.query.billingCustomers.findFirst({
where: and(
eq(billingCustomers.userId, userId),
eq(billingCustomers.organizationId, orgId)
),
});
if (existing) return existing;
// Fetch user details for Stripe customer creation
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
const stripeCustomer = await stripe.customers.create({
email: user.email,
name: user.name,
metadata: {
userId,
organizationId: orgId,
},
});
const [customer] = await db
.insert(billingCustomers)
.values({
userId,
organizationId: orgId,
stripeCustomerId: stripeCustomer.id,
})
.returning();
return customer;
}
}
ویب ہک ہینڈلر
ویب ہک ہینڈلر اسٹرائپ انضمام کا سب سے اہم حصہ ہے۔ ہر واقعہ بے ضمیر ہونا چاہیے:
// billing/webhook.controller.ts
import {
Controller,
Post,
Req,
Headers,
RawBodyRequest,
HttpCode,
} from '@nestjs/common';
import { Request } from 'express';
import Stripe from 'stripe';
import { stripe } from './stripe.client';
import { Public } from '../auth/decorators/public.decorator';
import { BillingWebhookService } from './billing-webhook.service';
@Controller('billing/webhook')
export class WebhookController {
constructor(private webhookService: BillingWebhookService) {}
@Post()
@Public()
@HttpCode(200) // Always return 200 — Stripe retries on non-2xx
async handleWebhook(
@Req() req: RawBodyRequest<Request>,
@Headers('stripe-signature') signature: string
) {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody!, // Requires raw body middleware
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
// Invalid signature — do not process
throw new Error(`Webhook signature verification failed: ${err.message}`);
}
// Route to specific handler
switch (event.type) {
case 'checkout.session.completed':
await this.webhookService.handleCheckoutCompleted(
event.data.object as Stripe.Checkout.Session
);
break;
case 'invoice.paid':
await this.webhookService.handleInvoicePaid(
event.data.object as Stripe.Invoice
);
break;
case 'invoice.payment_failed':
await this.webhookService.handlePaymentFailed(
event.data.object as Stripe.Invoice
);
break;
case 'customer.subscription.deleted':
await this.webhookService.handleSubscriptionDeleted(
event.data.object as Stripe.Subscription
);
break;
case 'customer.subscription.updated':
await this.webhookService.handleSubscriptionUpdated(
event.data.object as Stripe.Subscription
);
break;
case 'charge.refunded':
await this.webhookService.handleChargeRefunded(
event.data.object as Stripe.Charge
);
break;
default:
// Ignore unhandled event types
break;
}
return { received: true };
}
}
اہم: NestJS کو پٹی کے دستخط کی تصدیق کے لیے NestFactory.create() میں rawBody: true کی ضرورت ہے:
// main.ts
const app = await NestFactory.create(AppModule, {
rawBody: true,
});
چیک آؤٹ مکمل ہینڈلر
یہ بنیادی فراہمی کا ہینڈلر ہے۔ اسے کمزور بنائیں:
// billing/billing-webhook.service.ts
@Injectable()
export class BillingWebhookService {
async handleCheckoutCompleted(session: Stripe.Checkout.Session) {
// Check if already processed (idempotency)
const existing = await db.query.orders.findFirst({
where: eq(orders.stripeSessionId, session.id),
});
if (existing) {
return; // Already processed — Stripe is retrying
}
const { userId, organizationId, productId } = session.metadata!;
// Create order in transaction
await db.transaction(async (tx) => {
// Create order
const [order] = await tx
.insert(orders)
.values({
userId,
organizationId,
productId,
stripeSessionId: session.id,
stripePaymentIntentId: session.payment_intent as string,
amount: session.amount_total! / 100, // Stripe uses cents
currency: session.currency!,
status: 'completed',
})
.returning();
// Generate license key
const licenseKey = generateLicenseKey(productId);
// Create license
await tx.insert(licenses).values({
orderId: order.id,
userId,
organizationId,
productId,
licenseKey,
status: 'active',
issuedAt: new Date(),
});
});
// Send confirmation email (non-blocking — don't fail the webhook)
this.emailService
.sendPurchaseConfirmation(userId, productId)
.catch((err) => this.logger.error('Email send failed', err));
}
async handleInvoicePaid(invoice: Stripe.Invoice) {
// For subscriptions — renewal
if (!invoice.subscription) return;
// Find existing subscription/license and extend it
const license = await db.query.licenses.findFirst({
where: eq(licenses.stripeSubscriptionId, invoice.subscription as string),
});
if (!license) return;
await db
.update(licenses)
.set({
status: 'active',
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // +30 days
})
.where(eq(licenses.id, license.id));
// Log renewal in audit table
await db.insert(licenseAuditLogs).values({
licenseId: license.id,
action: 'renewed',
details: { invoiceId: invoice.id },
});
}
async handlePaymentFailed(invoice: Stripe.Invoice) {
if (!invoice.subscription) return;
const license = await db.query.licenses.findFirst({
where: eq(licenses.stripeSubscriptionId, invoice.subscription as string),
});
if (!license) return;
// Grace period before suspension (e.g., 3 days)
const gracePeriod = new Date();
gracePeriod.setDate(gracePeriod.getDate() + 3);
if (new Date() > gracePeriod) {
await db
.update(licenses)
.set({ status: 'suspended' })
.where(eq(licenses.id, license.id));
}
// Send dunning email
this.emailService
.sendPaymentFailedNotification(license.userId, invoice)
.catch(console.error);
}
async handleChargeRefunded(charge: Stripe.Charge) {
const order = await db.query.orders.findFirst({
where: eq(orders.stripePaymentIntentId, charge.payment_intent as string),
});
if (!order) return;
await db.transaction(async (tx) => {
// Update order status
await tx
.update(orders)
.set({ status: 'refunded' })
.where(eq(orders.id, order.id));
// Revoke associated licenses
await tx
.update(licenses)
.set({ status: 'revoked', revokedAt: new Date() })
.where(eq(licenses.orderId, order.id));
});
// Log audit trail
await db.insert(licenseAuditLogs).values({
licenseId: (await db.query.licenses.findFirst({
where: eq(licenses.orderId, order.id),
}))!.id,
action: 'revoked',
details: { reason: 'refund', chargeId: charge.id },
});
}
async handleSubscriptionDeleted(subscription: Stripe.Subscription) {
await db
.update(licenses)
.set({ status: 'cancelled', cancelledAt: new Date() })
.where(eq(licenses.stripeSubscriptionId, subscription.id));
}
async handleSubscriptionUpdated(subscription: Stripe.Subscription) {
const status = subscription.status === 'active' ? 'active' : 'suspended';
await db
.update(licenses)
.set({ status })
.where(eq(licenses.stripeSubscriptionId, subscription.id));
}
}
کسٹمر پورٹل
صارفین کو سپورٹ سے رابطہ کیے بغیر اپنی سبسکرپشنز کا انتظام کرنے کی اجازت دیں:
async createPortalSession(userId: string, orgId: string) {
const customer = await db.query.billingCustomers.findFirst({
where: and(
eq(billingCustomers.userId, userId),
eq(billingCustomers.organizationId, orgId)
),
});
if (!customer) {
throw new NotFoundException('No billing account found');
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: customer.stripeCustomerId,
return_url: `${process.env.FRONTEND_URL}/dashboard/billing`,
});
return { url: portalSession.url };
}
کسٹمر پورٹل صارفین کو سبسکرپشنز منسوخ کرنے، ادائیگی کے طریقوں کو اپ ڈیٹ کرنے، انوائسز ڈاؤن لوڈ کرنے، اور بلنگ کی تاریخ دیکھنے دیتا ہے — بغیر کسی حسب ضرورت UI کام کے۔
فرنٹ اینڈ انٹیگریشن
// web: lib/billing.ts
'use client';
import { apiFetch } from '@/lib/api';
export async function initiateCheckout(priceId: string, productId: string) {
const { url } = await apiFetch<{ url: string }>('/billing/checkout', {
method: 'POST',
body: JSON.stringify({ priceId, productId }),
});
// Redirect to Stripe Checkout
window.location.href = url;
}
export async function openCustomerPortal() {
const { url } = await apiFetch<{ url: string }>('/billing/portal', {
method: 'POST',
});
window.open(url, '_blank');
}
مقامی طور پر ویب ہکس کی جانچ کرنا
پٹی CLI مقامی ویب ہک کی ترقی کے لیے ضروری ہے:
# Install Stripe CLI
npm install -g stripe
# Listen and forward webhooks to localhost
stripe listen --forward-to localhost:3001/billing/webhook
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger invoice.paid
stripe trigger charge.refunded
stripe trigger customer.subscription.deleted
# View event logs
stripe events list
CLI آپ کے مقامی ویب ہک کا راز فراہم کرتا ہے — اسے اپنے .env.local میں استعمال کریں:
STRIPE_WEBHOOK_SECRET=whsec_test_... # From `stripe listen` output
مشترکہ نقصانات اور حل
خطرہ 1: ویب ہک کے بجائے چیک آؤٹ ری ڈائریکٹ پر رسائی فراہم کرنا
چیک آؤٹ کی کامیابی کو ری ڈائریکٹ کے ذریعے متحرک کیا جا سکتا ہے: براؤزر بیک/فارورڈ، ڈپلیکیٹ ٹیبز، یا نیٹ ورک دوبارہ کوشش کریں۔ ہمیشہ checkout.session.completed ویب ہک کی بنیاد پر فراہمی کریں، ری ڈائریکٹ URL کی نہیں۔
خطرہ 2: ویب ہک کی دوبارہ کوششوں کو نہ سنبھالنا (آدمی)
اسٹرائپ 72 گھنٹے تک ویب ہکس کو ناکام بنانے کی کوشش کی گئی۔ آئیڈیمپوٹینسی چیک کے بغیر، دوبارہ کوششیں ڈپلیکیٹ آرڈرز، لائسنس اور ای میلز بناتی ہیں۔ اسٹرائپ سیشن آئی ڈی کو اپنے آرڈر ٹیبل میں ایک منفرد رکاوٹ کے ساتھ اسٹور کریں — ڈپلیکیٹ انسرٹس خوبصورتی سے ناکام ہو جاتے ہیں۔
خطرہ 3: خام باڈی مڈل ویئر کا غلط استعمال کرنا
پٹی کے دستخط کی توثیق کے لیے خام، غیر تجزیہ شدہ درخواست کا باڈی درکار ہے۔ اگر ایکسپریس کا JSON باڈی پارسر پہلے چلتا ہے، تو خام باڈی ختم ہو جاتی ہے۔ NestJS میں rawBody: true استعمال کریں اور req.rawBody تک رسائی حاصل کریں، یا JSON مڈل ویئر کو خاص طور پر ویب ہک روٹ کے لیے خام باڈی کو محفوظ رکھنے کے لیے ترتیب دیں۔
خطرہ 4: پٹی API کالز کے لیے دوبارہ کوشش کرنے کی منطق غائب ہے
اسٹرائپ API کالز عارضی طور پر ناکام ہو سکتی ہیں۔ idempotent آپریشنز کے لیے دوبارہ کوشش کی منطق شامل کریں:
import Stripe from 'stripe';
async function retryStripeOperation<T>(
operation: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (err) {
if (err instanceof Stripe.errors.StripeConnectionError && attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * attempt));
continue;
}
throw err;
}
}
throw new Error('Max retries exceeded');
}
اکثر پوچھے گئے سوالات
کیا مجھے اسٹرائپ چیک آؤٹ یا حسب ضرورت ادائیگی کے فارم استعمال کرنے چاہئیں؟
ہمیشہ اسٹرائپ چیک آؤٹ کا استعمال کریں جب تک کہ آپ کے پاس مخصوص تقاضے نہ ہوں جو اسے ناممکن بناتی ہیں (نایاب)۔ چیک آؤٹ PCI کی تعمیل، 3D سیکیور تصدیق، Apple Pay، Google Pay، SEPA، اور درجنوں دیگر ادائیگی کے طریقے خود بخود ہینڈل کرتا ہے۔ حسب ضرورت ادائیگی کے فارمز (سٹرائپ ایلیمنٹس کا استعمال کرتے ہوئے) آپ سے PCI کی تعمیل خود برقرار رکھنے اور ادائیگی کے ہر طریقہ کو دستی طور پر لاگو کرنے کی ضرورت ہے۔ SaaS پروڈکٹس کے 95% کے لیے، چیک آؤٹ صحیح انتخاب ہے۔
میں اسٹرائپ کے ٹیسٹ موڈ بمقابلہ لائیو موڈ کو کیسے ہینڈل کروں؟
ماحول کی الگ الگ ترتیبیں استعمال کریں: STRIPE_SECRET_KEY=sk_test_... ترقی میں اور STRIPE_SECRET_KEY=sk_live_... پیداوار میں۔ سٹرائپ ٹیسٹ موڈ مکمل طور پر لائیو موڈ سے الگ تھلگ ہے — کوئی ٹیسٹ چارجز حقیقی رقم کو متاثر نہیں کرتا ہے اور ٹیسٹ موڈ میں کوئی لائیو ڈیٹا ظاہر نہیں ہوتا ہے۔ اپنے کوڈ میں stripe.mode استعمال کریں اس بات کی تصدیق کرنے کے لیے کہ آپ صحیح کلید استعمال کر رہے ہیں، اور اپنے منتظم UI میں ایک نمایاں اشارے شامل کریں جس سے یہ ظاہر ہو کہ آپ کس موڈ میں ہیں۔
اگر میرا ویب ہک سرور ڈاؤن ہو اور اسٹرائپ ڈیلیور نہ کر سکے تو کیا ہوگا؟
72 گھنٹے تک کے ایکسپونیشنل بیک آف شیڈول پر اسٹرائپ کی ناکام ویب ہکس کی دوبارہ کوششیں: 5 منٹ، 30 منٹ، 1 گھنٹہ، 2 گھنٹے، اور بڑھتے ہوئے وقفے۔ 72 گھنٹے کے بعد، ایونٹ کو ناکام سمجھا جاتا ہے۔ ناکام واقعات کو دستی طور پر دوبارہ ڈیلیور کرنے کے لیے اسٹرائپ کا ایونٹ ڈیش بورڈ استعمال کریں، یا stripe.events.list() سے استفسار کریں اور انہیں دوبارہ چلائیں۔ اپنے ہینڈلرز کو کمزور ہونے کے لیے ڈیزائن کریں تاکہ دوبارہ ڈیلیوری محفوظ رہے۔
میں میٹرڈ/استعمال پر مبنی بلنگ کیسے لاگو کروں؟
اسٹرائپ بلنگ stripe.subscriptionItems.createUsageRecord() کے ذریعے میٹرڈ استعمال کی حمایت کرتی ہے۔ ہر بلنگ کی مدت کے اختتام پر استعمال کی مقدار کے ساتھ استعمال کی اطلاع دیں۔ اپنے اسٹرائپ ڈیش بورڈ میں میٹرڈ پلانز کی قیمت "میٹرڈ" پرائسنگ کے بطور۔ کلیدی نفاذ کی تفصیل: بلنگ کی مدت ختم ہونے سے پہلے استعمال کی اطلاع دیں، اس کے بعد نہیں — اسٹرائپ مدت کے اختتام پر رسید کو حتمی شکل دیتی ہے۔
مجھے اپنے ڈیٹا بیس میں Stripe IDs کیسے ذخیرہ کرنا چاہیے؟
تمام Stripe IDs کو بطور text کالم اسٹور کریں (عددی نہیں)۔ stripe_customer_id، stripe_subscription_id، اور stripe_payment_intent_id پر اشاریہ جات بنائیں — آپ ان کے ذریعہ اکثر ریکارڈ تلاش کریں گے۔ جہاں مناسب ہو منفرد رکاوٹیں شامل کریں (فی صارف ایک صارف کا ریکارڈ)۔ اپنے ڈیٹا بیس میں کبھی بھی کریڈٹ کارڈ نمبر یا خام ادائیگی کا ڈیٹا ذخیرہ نہ کریں۔
اگلے اقدامات
اسٹرائپ بلنگ کو درست طریقے سے لاگو کرنا — ویب ہُک آئیڈیمپوٹینسی، سبسکرپشن لائف سائیکل مینجمنٹ، خودکار پروویژننگ، اور ریفنڈ ہینڈلنگ کے ساتھ — ایک غیر معمولی انجینئرنگ کا کام ہے۔ ECOSIRE خودکار لائسنس کی فراہمی اور کسٹمر پورٹل انضمام کے ساتھ پیداوار میں 6 اسٹرائپ ویب ہک ایونٹ کی اقسام کو ہینڈل کرتا ہے۔
اگر آپ کو اسٹرائپ انٹیگریشن، سبسکرپشن بلنگ انفراسٹرکچر، یا فل اسٹیک SaaS ڈیولپمنٹ سپورٹ کی ضرورت ہے تو، ہماری ڈیولپمنٹ سروسز کو دریافت کریں۔
تحریر
ECOSIRE Research and Development Team
ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔
متعلقہ مضامین
ERP for Law Firms: Case Management, Billing, and Client Portal
Complete guide to ERP for law firms — case management, legal billing, trust accounting, IOLTA compliance, client portal, and matter management for 2026.
Media Company ERP Implementation: Editorial, Ad Sales, and Subscriptions
Step-by-step guide to implementing ERP in media companies, covering editorial workflow integration, advertising system migration, subscription billing setup, and data migration.
Professional Services ERP Implementation: Time, Billing, and Projects
Step-by-step professional services ERP implementation guide covering time tracking, billing configuration, project accounting, and resource management setup.