Complete guide: Integrate LemonSqueezy with credits.dev for usage-based billing
Step-by-step tutorial on connecting LemonSqueezy payments to credits.dev metering. Handle subscriptions, one-time purchases, webhooks, and refunds with full code examples.
LemonSqueezy makes it easy to sell software products, and credits.dev makes it easy to track usage-based credits. This guide shows you how to integrate them so payments flow into credits automatically.
What you'll build
By the end of this tutorial, you'll have:
- LemonSqueezy checkout for subscriptions or one-time purchases
- Automatic credit grants when customers pay
- Webhook handlers that sync payments to credits.dev
- Refund handling that revokes credits
- Plan changes that adjust credit allowances
Prerequisites
- A LemonSqueezy account
- A credits.dev account
- Node.js 18+ and TypeScript
- Basic knowledge of webhooks and API integration
Architecture overview
The integration flow:
- Customer purchases a product or subscription on LemonSqueezy
- LemonSqueezy sends a webhook to your server
- Your webhook handler verifies the event and calls credits.dev to grant credits
- Your app checks credits.dev for real-time balance before allowing actions
- When subscriptions renew or customers refund, LemonSqueezy webhooks trigger credit adjustments
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ LemonSqueezy │─────▶│ Your Server │─────▶│ credits.dev │
│ (Payment) │ │ (Webhooks) │ │ (Credits) │
│ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘LemonSqueezy handles payment processing, subscriptions, and invoicing.
Your webhook handler receives payment events and translates them into credit operations.
credits.dev tracks balances, enforces limits, and manages usage in real time.
Step 1: Set up LemonSqueezy
1.1 Create products
In your LemonSqueezy dashboard, create products for each tier:
Example products:
- Starter Pack: $10 one-time purchase → 1,000 credits
- Pro Plan: $50/month subscription → 10,000 credits/month
- Enterprise Pack: $500 one-time → 100,000 credits
Screenshot placeholder: LemonSqueezy dashboard showing product creation with name "Pro Plan", price $50, and recurring billing set to monthly.
1.2 Get your API key
- Go to Settings → API in LemonSqueezy dashboard
- Create a new API key with read access
- Save it securely (you'll use it to verify webhooks)
Screenshot placeholder: LemonSqueezy API settings page with "Create API key" button highlighted.
1.3 Set up webhook endpoint
In LemonSqueezy Settings → Webhooks:
- Click "Add endpoint"
- Enter your webhook URL (e.g.,
https://yourdomain.com/webhooks/lemonsqueezy) - Select events to subscribe to:
order_created— one-time purchasessubscription_created— new subscriptionssubscription_updated— plan changessubscription_payment_success— recurring paymentssubscription_cancelled— cancellationsorder_refunded— refunds
- Save the signing secret (you'll use this to verify webhooks)
Screenshot placeholder: LemonSqueezy webhook configuration showing endpoint URL and selected events.
Step 2: Set up credits.dev
2.1 Create an account
Sign up at credits.dev and create your first project.
2.2 Get your API key
- Go to Settings → API Keys
- Create a new API key
- Save it securely
Screenshot placeholder: credits.dev dashboard showing API key creation with scopes selected.
2.3 Create plans (optional)
If you want recurring credit grants (monthly allowances), create plans in credits.dev:
- Go to Plans → Create Plan
- Name: "Pro Monthly"
- Amount: 10,000 credits
- Interval: month
- Accumulate: true (allow rollover) or false
You can also grant credits without plans (one-time grants).
Screenshot placeholder: credits.dev plan creation form showing "Pro Monthly" plan with 10,000 credits per month.
Step 3: Install dependencies
Install the required packages:
npm install @lemonsqueezy/lemonsqueezy.js @credits-dev/sdk cryptoOr with pnpm:
pnpm add @lemonsqueezy/lemonsqueezy.js @credits-dev/sdk cryptoStep 4: Create the webhook handler
Create a webhook endpoint that receives LemonSqueezy events and grants credits.
4.1 Verify webhook signatures
LemonSqueezy signs webhooks with HMAC. Always verify signatures to prevent fraud.
// lib/lemonsqueezy.ts
import crypto from "crypto"
export function verifyLemonSqueezySignature(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac("sha256", secret)
hmac.update(payload)
const digest = hmac.digest("hex")
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))
}4.2 Set up environment variables
Create a .env file:
# LemonSqueezy
LEMONSQUEEZY_API_KEY=your_lemonsqueezy_api_key
LEMONSQUEEZY_WEBHOOK_SECRET=your_webhook_signing_secret
# credits.dev
CREDITSDEV_API_KEY=your_creditsdev_api_key
# Optional: map LemonSqueezy product/variant IDs to credit amounts
# Format: VARIANT_ID:CREDITS,VARIANT_ID:CREDITS
LEMONSQUEEZY_CREDIT_MAPPING=12345:1000,67890:10000,11111:1000004.3 Create the webhook route
Here's a complete webhook handler for Next.js (App Router):
// app/api/webhooks/lemonsqueezy/route.ts
import { NextRequest, NextResponse } from "next/server"
import { CreditsClient } from "@credits-dev/sdk"
import { verifyLemonSqueezySignature } from "@/lib/lemonsqueezy"
const creditsClient = new CreditsClient({
apiKey: process.env.CREDITSDEV_API_KEY!,
})
// Parse credit mapping from env (variantId:credits)
const creditMapping = new Map<string, number>(
(process.env.LEMONSQUEEZY_CREDIT_MAPPING || "")
.split(",")
.filter(Boolean)
.map((pair) => {
const [variantId, credits] = pair.split(":")
return [variantId, parseInt(credits, 10)]
})
)
interface LemonSqueezyWebhookEvent {
meta: {
event_name: string
custom_data?: {
user_id?: string
account_id?: string
}
}
data: {
id: string
type: string
attributes: {
user_email: string
user_name: string
identifier: string
order_number: number
first_order_item: {
id: number
order_id: number
product_id: number
variant_id: number
product_name: string
variant_name: string
price: number
}
status: string
refunded: boolean
refunded_at: string | null
}
relationships?: {
customer?: {
data?: {
id: string
}
}
}
}
}
export async function POST(req: NextRequest) {
try {
// 1. Get raw body and signature
const rawBody = await req.text()
const signature = req.headers.get("x-signature")
if (!signature) {
console.error("Missing signature header")
return NextResponse.json({ error: "Missing signature" }, { status: 401 })
}
// 2. Verify signature
const isValid = verifyLemonSqueezySignature(
rawBody,
signature,
process.env.LEMONSQUEEZY_WEBHOOK_SECRET!
)
if (!isValid) {
console.error("Invalid webhook signature")
return NextResponse.json({ error: "Invalid signature" }, { status: 401 })
}
// 3. Parse event
const event: LemonSqueezyWebhookEvent = JSON.parse(rawBody)
const eventName = event.meta.event_name
console.log(`[LemonSqueezy] Received event: ${eventName}`)
// 4. Get customer identifier
// Use custom_data if you pass user_id during checkout, otherwise use email
const userId =
event.meta.custom_data?.user_id ||
event.meta.custom_data?.account_id ||
event.data.attributes.user_email
if (!userId) {
console.error("No user identifier found in webhook")
return NextResponse.json({ error: "No user identifier" }, { status: 400 })
}
// 5. Handle different event types
switch (eventName) {
case "order_created":
await handleOrderCreated(event, userId)
break
case "subscription_created":
await handleSubscriptionCreated(event, userId)
break
case "subscription_payment_success":
await handleSubscriptionPayment(event, userId)
break
case "subscription_updated":
await handleSubscriptionUpdated(event, userId)
break
case "subscription_cancelled":
await handleSubscriptionCancelled(event, userId)
break
case "order_refunded":
await handleOrderRefunded(event, userId)
break
default:
console.log(`[LemonSqueezy] Unhandled event: ${eventName}`)
}
return NextResponse.json({ success: true })
} catch (error) {
console.error("[LemonSqueezy] Webhook error:", error)
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
// ─── Event Handlers ──────────────────────────────────────────────────────
async function handleOrderCreated(
event: LemonSqueezyWebhookEvent,
userId: string
) {
const variantId = String(event.data.attributes.first_order_item.variant_id)
const credits = creditMapping.get(variantId)
if (!credits) {
console.warn(`[LemonSqueezy] No credit mapping for variant ${variantId}`)
return
}
console.log(`[LemonSqueezy] Granting ${credits} credits to ${userId}`)
// Ensure account exists in credits.dev
await ensureAccount(userId, event.data.attributes.user_email)
// Grant credits
const result = await creditsClient.addCredits({
externalId: userId,
amount: credits,
description: `Purchase: ${event.data.attributes.first_order_item.product_name}`,
paymentMethod: "lemonsqueezy",
metadata: {
lemonsqueezy_order_id: event.data.id,
order_number: event.data.attributes.order_number,
variant_id: variantId,
product_name: event.data.attributes.first_order_item.product_name,
},
})
console.log(`[LemonSqueezy] Credits granted. New balance: ${result.balance}`)
}
async function handleSubscriptionCreated(
event: LemonSqueezyWebhookEvent,
userId: string
) {
// For subscriptions, you might want to assign a plan in credits.dev
// that automatically grants credits monthly
console.log(`[LemonSqueezy] New subscription for ${userId}`)
await ensureAccount(userId, event.data.attributes.user_email)
// Option 1: Grant initial credits immediately
const variantId = String(event.data.attributes.first_order_item.variant_id)
const credits = creditMapping.get(variantId)
if (credits) {
await creditsClient.addCredits({
externalId: userId,
amount: credits,
description: `Subscription: ${event.data.attributes.first_order_item.product_name} (initial)`,
paymentMethod: "lemonsqueezy",
metadata: {
lemonsqueezy_subscription_id: event.data.id,
variant_id: variantId,
},
})
}
// Option 2: Assign a credits.dev plan for automatic monthly grants
// Uncomment if you've created plans in credits.dev
/*
const planId = getPlanIdForVariant(variantId)
if (planId) {
await creditsClient.assignPlanToAccount({
externalId: userId,
planId,
startMode: "immediate",
})
}
*/
}
async function handleSubscriptionPayment(
event: LemonSqueezyWebhookEvent,
userId: string
) {
// Recurring payment succeeded — grant credits for this period
const variantId = String(event.data.attributes.first_order_item.variant_id)
const credits = creditMapping.get(variantId)
if (!credits) {
console.warn(`[LemonSqueezy] No credit mapping for variant ${variantId}`)
return
}
console.log(`[LemonSqueezy] Subscription payment: granting ${credits} credits to ${userId}`)
await creditsClient.addCredits({
externalId: userId,
amount: credits,
description: `Subscription renewal: ${event.data.attributes.first_order_item.product_name}`,
paymentMethod: "lemonsqueezy",
metadata: {
lemonsqueezy_subscription_id: event.data.id,
order_number: event.data.attributes.order_number,
variant_id: variantId,
},
})
}
async function handleSubscriptionUpdated(
event: LemonSqueezyWebhookEvent,
userId: string
) {
// Handle plan upgrades/downgrades
console.log(`[LemonSqueezy] Subscription updated for ${userId}`)
// If the variant changed, you might want to:
// 1. Cancel the old plan in credits.dev
// 2. Assign the new plan
// 3. Grant prorated credits
// This depends on your business logic
}
async function handleSubscriptionCancelled(
event: LemonSqueezyWebhookEvent,
userId: string
) {
console.log(`[LemonSqueezy] Subscription cancelled for ${userId}`)
// If you assigned a credits.dev plan, cancel it here
// Or just stop granting credits — existing balance remains unless you deduct
}
async function handleOrderRefunded(
event: LemonSqueezyWebhookEvent,
userId: string
) {
console.log(`[LemonSqueezy] Order refunded for ${userId}`)
// Find the original credit grant and revoke it
// You can use metadata to find the transaction
const variantId = String(event.data.attributes.first_order_item.variant_id)
const credits = creditMapping.get(variantId)
if (credits) {
await creditsClient.deductCredits({
externalId: userId,
amount: credits,
description: `Refund: ${event.data.attributes.first_order_item.product_name}`,
metadata: {
lemonsqueezy_order_id: event.data.id,
refunded_at: event.data.attributes.refunded_at,
},
})
console.log(`[LemonSqueezy] Revoked ${credits} credits from ${userId}`)
}
}
// ─── Helpers ─────────────────────────────────────────────────────────────
async function ensureAccount(externalId: string, email: string) {
try {
// Try to get existing account
await creditsClient.getAccount({ externalId })
} catch (error) {
// Account doesn't exist, create it
console.log(`[credits.dev] Creating account for ${externalId}`)
await creditsClient.createAccount({
externalId,
email,
name: email.split("@")[0],
})
}
}4.4 Express.js alternative
If you're using Express instead of Next.js:
// routes/webhooks.ts
import express from "express"
import { CreditsClient } from "@credits-dev/sdk"
import { verifyLemonSqueezySignature } from "../lib/lemonsqueezy"
const router = express.Router()
const creditsClient = new CreditsClient({
apiKey: process.env.CREDITSDEV_API_KEY!,
})
router.post(
"/lemonsqueezy",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const signature = req.headers["x-signature"] as string
if (!signature) {
return res.status(401).json({ error: "Missing signature" })
}
const rawBody = req.body.toString("utf8")
const isValid = verifyLemonSqueezySignature(
rawBody,
signature,
process.env.LEMONSQUEEZY_WEBHOOK_SECRET!
)
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" })
}
const event = JSON.parse(rawBody)
const eventName = event.meta.event_name
console.log(`[LemonSqueezy] Received event: ${eventName}`)
// Handle events (same logic as Next.js version above)
// ...
res.json({ success: true })
} catch (error) {
console.error("[LemonSqueezy] Webhook error:", error)
res.status(500).json({ error: "Internal server error" })
}
}
)
export default routerStep 5: Pass user context to LemonSqueezy checkout
When creating a checkout link or overlay, pass your user's ID as custom data so webhooks can identify them.
5.1 Using LemonSqueezy overlay
// components/PricingCard.tsx
"use client"
import { useState } from "react"
interface PricingCardProps {
name: string
price: string
credits: number
checkoutUrl: string
userId: string
}
export function PricingCard({ name, price, credits, checkoutUrl, userId }: PricingCardProps) {
const [loading, setLoading] = useState(false)
const handlePurchase = async () => {
setLoading(true)
// Open LemonSqueezy checkout with custom data
// @ts-ignore - LemonSqueezy provides this globally
window.createLemonSqueezy()
// @ts-ignore
window.LemonSqueezy.Url.Open(checkoutUrl + `?checkout[custom][user_id]=${userId}`)
setLoading(false)
}
return (
<div className="border rounded-lg p-6">
<h3 className="text-xl font-bold">{name}</h3>
<p className="text-3xl font-bold mt-4">{price}</p>
<p className="text-gray-600 mt-2">{credits.toLocaleString()} credits</p>
<button
onClick={handlePurchase}
disabled={loading}
className="mt-6 w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"
>
{loading ? "Loading..." : "Purchase"}
</button>
</div>
)
}Load the LemonSqueezy script in your layout:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<script src="https://app.lemonsqueezy.com/js/lemon.js" defer />
</head>
<body>{children}</body>
</html>
)
}5.2 Using direct checkout URLs
Append custom data to the checkout URL:
const checkoutUrl = `https://yourstore.lemonsqueezy.com/checkout/buy/your-variant-id?checkout[custom][user_id]=${userId}`Step 6: Check balance before allowing actions
In your API routes or server actions, check credits.dev before allowing usage:
// app/api/ai/generate/route.ts
import { NextRequest, NextResponse } from "next/server"
import { CreditsClient } from "@credits-dev/sdk"
const creditsClient = new CreditsClient({
apiKey: process.env.CREDITSDEV_API_KEY!,
})
export async function POST(req: NextRequest) {
const { userId, prompt } = await req.json()
// 1. Check balance
const balance = await creditsClient.getBalance({ externalId: userId })
if (balance.available < 10) {
return NextResponse.json(
{ error: "Insufficient credits. Please purchase more." },
{ status: 402 }
)
}
// 2. Perform action (e.g., call AI API)
const result = await generateAIResponse(prompt)
// 3. Deduct credits
await creditsClient.deductCredits({
externalId: userId,
amount: 10,
description: "AI generation",
metadata: { prompt: prompt.slice(0, 100) },
})
return NextResponse.json({ result })
}Step 7: Display balance in your UI
Show the user's credit balance in your dashboard:
// components/BalanceWidget.tsx
"use client"
import { useEffect, useState } from "react"
interface BalanceWidgetProps {
userId: string
}
export function BalanceWidget({ userId }: BalanceWidgetProps) {
const [balance, setBalance] = useState<number | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchBalance() {
try {
const res = await fetch(`/api/credits/balance?userId=${userId}`)
const data = await res.json()
setBalance(data.balance)
} catch (error) {
console.error("Failed to fetch balance:", error)
} finally {
setLoading(false)
}
}
fetchBalance()
// Poll every 30 seconds
const interval = setInterval(fetchBalance, 30000)
return () => clearInterval(interval)
}, [userId])
if (loading) return <div>Loading...</div>
return (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500">Available Credits</h3>
<p className="text-3xl font-bold mt-2">
{balance?.toLocaleString() ?? "—"}
</p>
<a
href="/pricing"
className="mt-4 inline-block text-blue-600 hover:underline"
>
Purchase more →
</a>
</div>
)
}API route to fetch balance:
// app/api/credits/balance/route.ts
import { NextRequest, NextResponse } from "next/server"
import { CreditsClient } from "@credits-dev/sdk"
const creditsClient = new CreditsClient({
apiKey: process.env.CREDITSDEV_API_KEY!,
})
export async function GET(req: NextRequest) {
const userId = req.nextUrl.searchParams.get("userId")
if (!userId) {
return NextResponse.json({ error: "Missing userId" }, { status: 400 })
}
try {
const balance = await creditsClient.getBalance({ externalId: userId })
return NextResponse.json({
balance: balance.available,
reserved: balance.reserved,
})
} catch (error) {
console.error("Failed to fetch balance:", error)
return NextResponse.json(
{ error: "Failed to fetch balance" },
{ status: 500 }
)
}
}Step 8: Testing the integration
8.1 Use LemonSqueezy test mode
LemonSqueezy provides a test mode. Enable it in your dashboard and use test card details:
- Card number:
4242 4242 4242 4242 - Expiry: Any future date
- CVC: Any 3 digits
8.2 Test webhook locally with ngrok
- Install ngrok:
npm install -g ngrok - Start your dev server:
npm run dev - Expose it:
ngrok http 3000 - Update your LemonSqueezy webhook URL to the ngrok URL +
/api/webhooks/lemonsqueezy
8.3 Verify events in dashboards
After a test purchase:
- Check LemonSqueezy dashboard → Webhooks → Recent deliveries
- Check credits.dev dashboard → Transactions
- Verify the account balance increased
Screenshot placeholder: LemonSqueezy webhook delivery log showing successful 200 response.
Screenshot placeholder: credits.dev transactions page showing new credit grant with LemonSqueezy metadata.
8.4 Test refunds
- In LemonSqueezy, refund a test order
- Verify the webhook fired
- Check credits.dev to ensure credits were deducted
Step 9: Production considerations
9.1 Idempotency
LemonSqueezy may retry webhooks. To prevent duplicate credit grants, track processed events:
// Store in your database
interface ProcessedWebhook {
id: string // event.data.id from LemonSqueezy
event_name: string
processed_at: Date
}
async function hasProcessedEvent(eventId: string): Promise<boolean> {
// Query your database
const existing = await db.processedWebhooks.findUnique({
where: { id: eventId },
})
return !!existing
}
async function markEventProcessed(eventId: string, eventName: string) {
await db.processedWebhooks.create({
data: { id: eventId, event_name: eventName, processed_at: new Date() },
})
}
// In your webhook handler:
if (await hasProcessedEvent(event.data.id)) {
console.log(`[LemonSqueezy] Event ${event.data.id} already processed`)
return NextResponse.json({ success: true })
}
// ... process event ...
await markEventProcessed(event.data.id, eventName)9.2 Error handling and retries
If granting credits fails, return a 500 status so LemonSqueezy retries:
try {
await creditsClient.addCredits({ ... })
} catch (error) {
console.error("Failed to grant credits:", error)
return NextResponse.json(
{ error: "Failed to grant credits" },
{ status: 500 } // LemonSqueezy will retry
)
}9.3 Logging and monitoring
Log all webhook events for debugging:
console.log("[LemonSqueezy]", {
event: eventName,
userId,
variantId,
credits,
timestamp: new Date().toISOString(),
})
// Use a service like Sentry, LogRocket, or Datadog in production9.4 Secure your webhook endpoint
- Always verify signatures
- Use HTTPS in production
- Don't expose sensitive data in responses
9.5 Handle edge cases
- Customer already has credits: Additive by default (stacks)
- Subscription paused: No credits granted until resumed
- Partial refunds: LemonSqueezy doesn't support this natively; handle manually
Step 10: Advanced patterns
10.1 Trial periods with credits
Give new users a one-time credit grant:
async function ensureAccount(externalId: string, email: string) {
try {
await creditsClient.getAccount({ externalId })
} catch (error) {
// New account — grant trial credits
await creditsClient.createAccount({
externalId,
email,
name: email.split("@")[0],
})
await creditsClient.addCredits({
externalId,
amount: 500,
description: "Welcome bonus",
metadata: { type: "trial" },
})
console.log(`[credits.dev] Granted 500 trial credits to ${externalId}`)
}
}10.2 Credit packs with bonuses
Offer "buy 1,000 credits, get 1,100" by adjusting the credit mapping:
# .env
# variant_id:credits (grants 10% bonus)
LEMONSQUEEZY_CREDIT_MAPPING=12345:1100,67890:1100010.3 Usage analytics
Track which features consume the most credits:
await creditsClient.deductCredits({
externalId: userId,
amount: 10,
description: "AI generation",
metadata: {
feature: "text-to-image",
model: "dall-e-3",
},
})
// Later, query transactions to analyze usage by feature
const transactions = await creditsClient.listTransactions({
externalId: userId,
limit: 100,
})10.4 Low balance notifications
Set up a webhook in credits.dev or poll periodically:
const balance = await creditsClient.getBalance({ externalId: userId })
if (balance.available < 100) {
await sendLowBalanceEmail(userId, balance.available)
}Complete example repository structure
my-app/
├── app/
│ ├── api/
│ │ ├── webhooks/
│ │ │ └── lemonsqueezy/
│ │ │ └── route.ts # Webhook handler
│ │ ├── credits/
│ │ │ └── balance/
│ │ │ └── route.ts # Balance API
│ │ └── ai/
│ │ └── generate/
│ │ └── route.ts # Usage endpoint
│ ├── pricing/
│ │ └── page.tsx # Pricing page
│ └── dashboard/
│ └── page.tsx # User dashboard with balance
├── components/
│ ├── PricingCard.tsx # LemonSqueezy checkout
│ └── BalanceWidget.tsx # Display credits
├── lib/
│ ├── lemonsqueezy.ts # Signature verification
│ └── credits.ts # credits.dev client wrapper
└── .env.local # Environment variablesTroubleshooting
Webhooks not firing
- Check LemonSqueezy dashboard → Webhooks → Recent deliveries for errors
- Verify your webhook URL is publicly accessible (use ngrok for local testing)
- Ensure your endpoint returns 200 status
Credits not granted
- Check your server logs for errors
- Verify the variant ID mapping in your
.env - Check credits.dev dashboard → Transactions for failed attempts
Signature verification fails
- Ensure you're using the raw request body (not parsed JSON)
- Verify the webhook secret matches LemonSqueezy settings
- Check for trailing whitespace or encoding issues
Duplicate credit grants
- Implement idempotency (Step 9.1)
- Check if webhooks are being sent to multiple endpoints
Next steps
You now have a complete integration between LemonSqueezy and credits.dev!
Ideas to extend this:
- Add usage dashboards — show credit consumption over time
- Implement low balance warnings — email users when credits run low
- Create admin tools — manually adjust credits for support cases
- Add referral bonuses — grant credits when users refer friends
- Tier-based pricing — use credits.dev plans for monthly allowances
Resources
- LemonSqueezy API Docs
- credits.dev Documentation
- credits.dev TypeScript SDK
- Example repository (coming soon)
Get help
If you found this guide helpful, consider sharing it with other developers building usage-based products. And if you build something cool with this integration, we'd love to hear about it!