Docus Logo
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

  1. Go to your LemonSqueezy store
  2. Create a product with price in PKR
  3. 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

What Comes Next