Charcoles Payments
Examples
Copy-paste ready examples for common payment scenarios.
Example 1: Full Stripe Payment Flow
Create a payment intent, confirm with Stripe.js, and handle the webhook.
Backend: Create Intent
curl -X POST http://localhost:3000/payments/create-intent \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"amount": 2999,
"currency": "usd",
"metadata": {"orderId": "order_123"}
}'
Response:
{
"success": true,
"data": {
"id": "pi_3abc...",
"clientSecret": "pi_3abc..._secret_xyz",
"status": "requires_payment_method",
"amount": 2999,
"currency": "usd"
}
}
Frontend: Confirm Payment (example with Stripe.js)
// Frontend only — not part of the backend docs
// But this is what happens with the clientSecret
const { clientSecret } = response.data
const { error } = await stripe.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: 'https://yoursite.com/success',
},
})
Backend: Handle Webhook
When the user completes payment on Stripe's page, Stripe fires a payment_intent.succeeded webhook:
// In your payments.controller.js webhook handler
import { PAYMENT_EVENTS } from '@charcoles/payments'
export const handlePaymentWebhook = async (req, res) => {
const result = await paymentService.processWebhook(req, res)
switch (result.event) {
case PAYMENT_EVENTS.STRIPE_PAYMENT_SUCCEEDED:
// Payment confirmed — fulfill the order
const { orderId, amount, currency } = result.data
await sendConfirmationEmail(orderId)
await updateOrderStatus(orderId, 'paid')
await grantProductAccess(orderId)
break
case PAYMENT_EVENTS.STRIPE_PAYMENT_FAILED:
const { orderId } = result.data
await notifyCustomerOfFailure(orderId)
break
}
return res.status(200).json({ received: true })
}
Example 2: Full LemonSqueezy Checkout Flow
Create a payment intent with a variant ID and redirect to LemonSqueezy's hosted checkout.
Backend: Create Intent with Variant
curl -X POST http://localhost:3000/payments/create-intent \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"amount": 2999,
"currency": "usd",
"metadata": {
"variantId": "78901",
"orderId": "order_456"
}
}'
Response:
{
"success": true,
"data": {
"id": "abc123",
"checkoutUrl": "https://store.lemonsqueezy.com/checkout/buy/...",
"status": "created",
"amount": 2999,
"currency": "usd"
}
}
Frontend: Redirect to Checkout
// Frontend: redirect user to the checkout URL
window.location.href = response.data.checkoutUrl
Backend: Handle Webhook
When the user completes checkout on LemonSqueezy, they fire an order_created webhook:
import { PAYMENT_EVENTS } from '@charcoles/payments'
export const handlePaymentWebhook = async (req, res) => {
const result = await paymentService.processWebhook(req, res)
switch (result.event) {
case PAYMENT_EVENTS.LS_ORDER_CREATED:
// Payment confirmed
const { orderId, amount, currency } = result.data
await sendConfirmationEmail(orderId)
await updateOrderStatus(orderId, 'paid')
await grantProductAccess(orderId)
break
case PAYMENT_EVENTS.LS_ORDER_REFUNDED:
const { orderId } = result.data
await revokeProductAccess(orderId)
break
}
return res.status(200).json({ received: true })
}
Example 3: Full Refund
curl -X POST http://localhost:3000/payments/refund \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{"paymentId": "pi_3abc..."}'
Response:
{
"success": true,
"data": {
"id": "re_456...",
"status": "succeeded",
"amount": 2999
}
}
Example 4: Partial Refund ($10.00)
curl -X POST http://localhost:3000/payments/refund \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"paymentId": "pi_3abc...",
"amount": 1000
}'
Response:
{
"success": true,
"data": {
"id": "re_457...",
"status": "succeeded",
"amount": 1000
}
}
Example 5: Check Payment Status
curl -X GET http://localhost:3000/payments/status/pi_3abc... \
-H "Authorization: Bearer your-jwt-token"
Response:
{
"success": true,
"data": {
"id": "pi_3abc...",
"status": "paid",
"amount": 2999,
"currency": "usd"
}
}
Example 6: Custom Webhook Fulfillment Logic
Here's a realistic webhook handler with email, database updates, and product access:
import { PAYMENT_EVENTS } from '@charcoles/payments'
import { sendEmail } from '@/services/email'
import { Order } from '@/models/order'
import { User } from '@/models/user'
export const handlePaymentWebhook = async (req, res) => {
const result = await paymentService.processWebhook(req, res)
try {
switch (result.event) {
case PAYMENT_EVENTS.STRIPE_PAYMENT_SUCCEEDED:
case PAYMENT_EVENTS.LS_ORDER_CREATED:
// Get order data
const order = await Order.findById(result.data.orderId)
const user = await User.findById(order.userId)
// Update order status
order.status = 'paid'
order.paidAt = new Date()
order.paymentId = result.data.id
await order.save()
// Grant product access
if (order.product === 'pro-plan') {
user.plan = 'pro'
user.planExpiresAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
await user.save()
}
// Send confirmation email
await sendEmail({
to: user.email,
template: 'payment-confirmed',
data: {
userName: user.name,
orderId: order._id,
amount: (result.data.amount / 100).toFixed(2),
currency: result.data.currency.toUpperCase(),
},
})
console.log(`Order ${order._id} fulfilled`)
break
case PAYMENT_EVENTS.STRIPE_PAYMENT_FAILED:
// Notify customer of failure
const failedOrder = await Order.findById(result.data.orderId)
const failedUser = await User.findById(failedOrder.userId)
await sendEmail({
to: failedUser.email,
template: 'payment-failed',
data: {
userName: failedUser.name,
orderId: failedOrder._id,
},
})
failedOrder.status = 'failed'
await failedOrder.save()
break
case PAYMENT_EVENTS.LS_ORDER_REFUNDED:
case PAYMENT_EVENTS.STRIPE_REFUND_CREATED:
// Handle refund
const refundedOrder = await Order.findById(result.data.orderId)
const refundedUser = await User.findById(refundedOrder.userId)
refundedOrder.status = 'refunded'
await refundedOrder.save()
// Revoke product access
if (refundedUser.plan === 'pro') {
refundedUser.plan = 'free'
refundedUser.planExpiresAt = null
await refundedUser.save()
}
// Notify customer
await sendEmail({
to: refundedUser.email,
template: 'refund-processed',
data: {
userName: refundedUser.name,
orderId: refundedOrder._id,
amount: (result.data.amount / 100).toFixed(2),
},
})
console.log(`Order ${refundedOrder._id} refunded`)
break
}
return res.status(200).json({ received: true })
} catch (error) {
// Log the error but still return 200 to prevent retry storms
console.error(`Webhook processing error: ${error.message}`)
return res.status(200).json({ received: true, error: error.message })
}
}
Example 7: PKR Payments with LemonSqueezy
For Pakistani developers receiving payments in PKR:
Environment Setup
PAYMENT_PROVIDER=lemonsqueezy
LEMONSQUEEZY_API_KEY=your_api_key
LEMONSQUEEZY_WEBHOOK_SECRET=your_webhook_secret
LEMONSQUEEZY_STORE_ID=12345
Create a Product in LemonSqueezy Store
- Go to your LemonSqueezy store
- Create a product with price in PKR
- Copy the variant ID
Create Payment (Backend)
curl -X POST http://localhost:3000/payments/create-intent \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"currency": "pkr",
"metadata": {
"variantId": "78901",
"orderId": "order_pakistan_001"
}
}'
Note: 500000 paisas = PKR 5,000 (1 rupee = 100 paisas)
Handle Webhook
case PAYMENT_EVENTS.LS_ORDER_CREATED:
// Same fulfillment logic as other examples
// Currency is automatically PKR
const { orderId, amount, currency } = result.data
// amount is in paisas, convert for display: amount / 100 = PKR
const amountInPKR = amount / 100
await sendConfirmationEmail(orderId, amountInPKR)
break
Example 8: Tiered Pricing with Multiple Variants
For SaaS with multiple subscription tiers using LemonSqueezy:
Create Different Product Variants in LemonSqueezy
- Basic Plan: variant ID
1001— PKR 2,000/month - Pro Plan: variant ID
1002— PKR 5,000/month - Enterprise Plan: variant ID
1003— Contact sales
Create Payment Based on Selected Plan
# User selects Pro Plan
curl -X POST http://localhost:3000/payments/create-intent \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"currency": "pkr",
"metadata": {
"variantId": "1002",
"planType": "pro",
"orderId": "order_user_123"
}
}'
Handle Webhook and Grant Tier Access
case PAYMENT_EVENTS.LS_ORDER_CREATED:
const planType = result.data.metadata.planType
const user = await User.findById(result.data.userId)
switch (planType) {
case 'pro':
user.subscriptionTier = 'pro'
user.features = ['feature_a', 'feature_b', 'feature_c']
break
case 'basic':
user.subscriptionTier = 'basic'
user.features = ['feature_a']
break
}
await user.save()
break