init
This commit is contained in:
139
.opencode/skills/payment-integration/references/creem/api.md
Normal file
139
.opencode/skills/payment-integration/references/creem/api.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Creem.io API Reference
|
||||
|
||||
## Checkout Sessions
|
||||
|
||||
### Create Checkout Session
|
||||
|
||||
```javascript
|
||||
// POST /v1/checkout/sessions
|
||||
const session = await creem.checkout.sessions.create({
|
||||
product_id: 'prod_xxx',
|
||||
success_url: 'https://example.com/success',
|
||||
cancel_url: 'https://example.com/cancel',
|
||||
customer_email: 'user@example.com', // Optional
|
||||
metadata: { order_id: '123' } // Optional
|
||||
});
|
||||
// Returns: { url: 'https://checkout.creem.io/xxx', id: 'cs_xxx' }
|
||||
```
|
||||
|
||||
## Products
|
||||
|
||||
### Create Product
|
||||
|
||||
```javascript
|
||||
// POST /v1/products
|
||||
const product = await creem.products.create({
|
||||
name: 'Pro Plan',
|
||||
description: 'Full access to all features',
|
||||
price: 2900, // Amount in cents
|
||||
currency: 'usd',
|
||||
recurring: { // Optional - for subscriptions
|
||||
interval: 'month',
|
||||
interval_count: 1
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Retrieve Product
|
||||
|
||||
```javascript
|
||||
// GET /v1/products/:id
|
||||
const product = await creem.products.retrieve('prod_xxx');
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
### Retrieve Transaction
|
||||
|
||||
```javascript
|
||||
// GET /v1/transactions/:id
|
||||
const transaction = await creem.transactions.retrieve('txn_xxx');
|
||||
```
|
||||
|
||||
### List Transactions
|
||||
|
||||
```javascript
|
||||
// GET /v1/transactions
|
||||
const transactions = await creem.transactions.list({
|
||||
customer_id: 'cus_xxx', // Optional filter
|
||||
product_id: 'prod_xxx', // Optional filter
|
||||
status: 'completed', // Optional filter
|
||||
limit: 25,
|
||||
starting_after: 'txn_xxx' // Pagination cursor
|
||||
});
|
||||
```
|
||||
|
||||
## Customers
|
||||
|
||||
### Retrieve Customer
|
||||
|
||||
```javascript
|
||||
// GET /v1/customers/:id
|
||||
const customer = await creem.customers.retrieve('cus_xxx');
|
||||
|
||||
// GET /v1/customers/email/:email
|
||||
const customer = await creem.customers.retrieveByEmail('user@example.com');
|
||||
```
|
||||
|
||||
### List Customers
|
||||
|
||||
```javascript
|
||||
// GET /v1/customers
|
||||
const customers = await creem.customers.list({
|
||||
limit: 25,
|
||||
starting_after: 'cus_xxx'
|
||||
});
|
||||
```
|
||||
|
||||
### Generate Portal Link
|
||||
|
||||
```javascript
|
||||
// POST /v1/customers/:id/portal
|
||||
const portal = await creem.customers.createPortalSession('cus_xxx');
|
||||
// Returns: { url: 'https://portal.creem.io/xxx' }
|
||||
```
|
||||
|
||||
## Discount Codes
|
||||
|
||||
### Create Discount
|
||||
|
||||
```javascript
|
||||
// POST /v1/discounts
|
||||
const discount = await creem.discounts.create({
|
||||
code: 'LAUNCH20',
|
||||
type: 'percentage', // or 'fixed'
|
||||
value: 20, // 20% or 20 cents
|
||||
expires_at: '2024-12-31T23:59:59Z',
|
||||
max_redemptions: 100 // Optional
|
||||
});
|
||||
```
|
||||
|
||||
### Retrieve Discount
|
||||
|
||||
```javascript
|
||||
// GET /v1/discounts/:code
|
||||
const discount = await creem.discounts.retrieve('LAUNCH20');
|
||||
```
|
||||
|
||||
### Delete Discount
|
||||
|
||||
```javascript
|
||||
// DELETE /v1/discounts/:code
|
||||
await creem.discounts.delete('LAUNCH20');
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const session = await creem.checkout.sessions.create({...});
|
||||
} catch (error) {
|
||||
if (error.type === 'invalid_request_error') {
|
||||
console.error('Invalid parameters:', error.message);
|
||||
} else if (error.type === 'authentication_error') {
|
||||
console.error('Invalid API key');
|
||||
} else if (error.type === 'rate_limit_error') {
|
||||
console.error('Rate limited, retry after:', error.retry_after);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# Creem.io Checkouts
|
||||
|
||||
## Checkout Options
|
||||
|
||||
1. **Programmatic Sessions** - Full API control
|
||||
2. **Checkout Links** - No-code, shareable URLs
|
||||
3. **Storefronts** - Hosted product pages
|
||||
|
||||
## Create Checkout Session
|
||||
|
||||
```javascript
|
||||
const session = await creem.checkout.sessions.create({
|
||||
product_id: 'prod_xxx',
|
||||
success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
|
||||
cancel_url: 'https://example.com/cancel',
|
||||
|
||||
// Optional parameters
|
||||
customer_email: 'user@example.com',
|
||||
customer_id: 'cus_xxx', // Existing customer
|
||||
quantity: 1, // For seat-based products
|
||||
discount_code: 'LAUNCH20', // Pre-apply discount
|
||||
metadata: {
|
||||
order_id: '123',
|
||||
referral_code: 'abc'
|
||||
},
|
||||
|
||||
// Custom fields
|
||||
custom_fields: [
|
||||
{ key: 'company', label: 'Company Name', required: true }
|
||||
]
|
||||
});
|
||||
|
||||
// Redirect user to checkout
|
||||
redirect(session.url);
|
||||
```
|
||||
|
||||
## Checkout Customization
|
||||
|
||||
Configure in dashboard or via API:
|
||||
|
||||
- **Branding**: Logo, colors, themes
|
||||
- **Email Receipts**: Custom templates
|
||||
- **Localization**: Auto-detect or force language (42 supported)
|
||||
- **Custom Fields**: Collect additional data
|
||||
|
||||
## Retrieve Session
|
||||
|
||||
```javascript
|
||||
// GET /v1/checkout/sessions/:id
|
||||
const session = await creem.checkout.sessions.retrieve('cs_xxx');
|
||||
// Returns: { id, status, customer_id, product_id, amount, metadata, ... }
|
||||
```
|
||||
|
||||
## Success URL Parameters
|
||||
|
||||
Creem replaces `{CHECKOUT_SESSION_ID}` in success URL:
|
||||
|
||||
```javascript
|
||||
// Frontend: parse session ID from URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionId = urlParams.get('session_id');
|
||||
|
||||
// Backend: verify and fulfill
|
||||
const session = await creem.checkout.sessions.retrieve(sessionId);
|
||||
if (session.status === 'complete') {
|
||||
await fulfillOrder(session);
|
||||
}
|
||||
```
|
||||
|
||||
## No-Code Checkout Links
|
||||
|
||||
Create in dashboard - shareable URLs for any product. Good for:
|
||||
- Social media links
|
||||
- Email campaigns
|
||||
- Quick sales without integration
|
||||
|
||||
## Storefronts
|
||||
|
||||
Hosted product pages - display multiple products without custom website:
|
||||
|
||||
1. Configure storefront in dashboard
|
||||
2. Add products to display
|
||||
3. Share storefront URL
|
||||
4. Customers browse and checkout
|
||||
|
||||
## Cart Abandonment Recovery
|
||||
|
||||
Enable in dashboard - automatic emails sent when checkout abandoned:
|
||||
- Configurable delay before sending
|
||||
- Customizable email content
|
||||
- Include discount code incentive
|
||||
|
||||
## Embedding (Coming)
|
||||
|
||||
For embedded checkout in your site, see SDK adapters:
|
||||
- Next.js Adapter
|
||||
- React components
|
||||
|
||||
See `references/creem/sdk.md` for implementation details.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Creem.io Licensing
|
||||
|
||||
Software licensing with device activation management.
|
||||
|
||||
## License Flow
|
||||
|
||||
```
|
||||
purchase → license_key issued → activate device → validate → deactivate
|
||||
```
|
||||
|
||||
## Activate License
|
||||
|
||||
Register a device against a license key:
|
||||
|
||||
```javascript
|
||||
// POST /v1/licenses/activate
|
||||
const activation = await creem.licenses.activate({
|
||||
license_key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
instance_id: 'device_fingerprint_123', // Unique device identifier
|
||||
instance_name: 'MacBook Pro' // Optional friendly name
|
||||
});
|
||||
|
||||
// Returns: {
|
||||
// id: 'act_xxx',
|
||||
// license_key: '...',
|
||||
// instance_id: '...',
|
||||
// activated_at: '...',
|
||||
// valid_until: '...'
|
||||
// }
|
||||
```
|
||||
|
||||
## Validate License
|
||||
|
||||
Check if license is active for specific device:
|
||||
|
||||
```javascript
|
||||
// POST /v1/licenses/validate
|
||||
const validation = await creem.licenses.validate({
|
||||
license_key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
instance_id: 'device_fingerprint_123'
|
||||
});
|
||||
|
||||
// Returns: {
|
||||
// valid: true,
|
||||
// license_key: '...',
|
||||
// product_id: 'prod_xxx',
|
||||
// customer_id: 'cus_xxx',
|
||||
// expires_at: '2025-01-15T00:00:00Z',
|
||||
// activations_used: 2,
|
||||
// activations_limit: 5
|
||||
// }
|
||||
```
|
||||
|
||||
## Deactivate License
|
||||
|
||||
Remove device activation to free slot:
|
||||
|
||||
```javascript
|
||||
// POST /v1/licenses/deactivate
|
||||
await creem.licenses.deactivate({
|
||||
license_key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
instance_id: 'device_fingerprint_123'
|
||||
});
|
||||
```
|
||||
|
||||
## Client-Side Implementation
|
||||
|
||||
```javascript
|
||||
// Desktop app example (Electron, Tauri, etc.)
|
||||
class LicenseManager {
|
||||
constructor(apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
this.instanceId = this.getDeviceFingerprint();
|
||||
}
|
||||
|
||||
getDeviceFingerprint() {
|
||||
// Generate unique device ID (machine ID, hardware hash, etc.)
|
||||
return require('node-machine-id').machineIdSync();
|
||||
}
|
||||
|
||||
async activate(licenseKey) {
|
||||
const response = await fetch('https://api.creem.io/v1/licenses/activate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
license_key: licenseKey,
|
||||
instance_id: this.instanceId,
|
||||
instance_name: os.hostname()
|
||||
})
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async validate(licenseKey) {
|
||||
const response = await fetch('https://api.creem.io/v1/licenses/validate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
license_key: licenseKey,
|
||||
instance_id: this.instanceId
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.valid;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Activation Limits
|
||||
|
||||
Configure per product - limits simultaneous device activations:
|
||||
|
||||
```javascript
|
||||
const product = await creem.products.create({
|
||||
name: 'Desktop App License',
|
||||
price: 4900,
|
||||
currency: 'usd',
|
||||
license_config: {
|
||||
activations_limit: 3 // Max 3 devices per license
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## License Events (Webhooks)
|
||||
|
||||
- `license.activated` - Device activated
|
||||
- `license.deactivated` - Device deactivated
|
||||
- `license.expired` - License expired (subscription ended)
|
||||
|
||||
See `references/creem/webhooks.md` for webhook handling.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Creem.io Overview
|
||||
|
||||
Payment infrastructure platform supporting subscriptions, one-time payments, and licensing. Functions as Merchant of Record (MoR) - handles compliance, taxes, and payment processing.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Merchant of Record**: Tax compliance, payment processing, global coverage
|
||||
- **Subscriptions**: Recurring billing, trials, seat-based, prorations
|
||||
- **One-Time Payments**: Single charges for products/services
|
||||
- **Licensing**: Activation keys, device management, validation
|
||||
- **Checkouts**: Hosted, embedded, no-code options
|
||||
- **Customer Portal**: Self-service billing management
|
||||
- **Revenue Splits**: Automatic payment distribution to multiple recipients
|
||||
|
||||
## When to Choose Creem
|
||||
|
||||
- Global SaaS products requiring MoR
|
||||
- Software licensing with activation management
|
||||
- Subscription products with seat-based billing
|
||||
- Digital product sales with file delivery
|
||||
- Affiliate/commission programs
|
||||
- Multi-recipient revenue splitting
|
||||
|
||||
## Authentication
|
||||
|
||||
```bash
|
||||
# API Key authentication
|
||||
curl -H "Authorization: Bearer sk_live_xxx" https://api.creem.io/v1/...
|
||||
```
|
||||
|
||||
Environment variables:
|
||||
```bash
|
||||
CREEM_API_KEY=sk_live_xxx # Production
|
||||
CREEM_API_KEY=sk_test_xxx # Test mode
|
||||
CREEM_WEBHOOK_SECRET=whsec_xxx # Webhook verification
|
||||
```
|
||||
|
||||
## API Base URLs
|
||||
|
||||
- **Production**: `https://api.creem.io/v1`
|
||||
- **Test Mode**: Use `sk_test_` prefixed API keys
|
||||
|
||||
## Rate Limits
|
||||
|
||||
Standard API rate limits apply. Check response headers for limit status.
|
||||
|
||||
## Global Support
|
||||
|
||||
- **Customers**: Hundreds of countries supported
|
||||
- **Merchants**: Global payouts
|
||||
- **Languages**: 42 languages for checkout localization
|
||||
|
||||
## Related References
|
||||
|
||||
- **API Endpoints**: `references/creem/api.md`
|
||||
- **Webhooks**: `references/creem/webhooks.md`
|
||||
- **Checkouts**: `references/creem/checkouts.md`
|
||||
- **Subscriptions**: `references/creem/subscriptions.md`
|
||||
- **Licensing**: `references/creem/licensing.md`
|
||||
- **SDKs**: `references/creem/sdk.md`
|
||||
|
||||
## External Resources
|
||||
|
||||
- **Documentation**: https://docs.creem.io
|
||||
- **LLM Docs**: https://docs.creem.io/llms.txt
|
||||
161
.opencode/skills/payment-integration/references/creem/sdk.md
Normal file
161
.opencode/skills/payment-integration/references/creem/sdk.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Creem.io SDKs
|
||||
|
||||
## Official SDKs
|
||||
|
||||
### Core SDK (`creem`)
|
||||
|
||||
Full API access with maximum flexibility:
|
||||
|
||||
```bash
|
||||
npm install creem
|
||||
# or
|
||||
pip install creem
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Node.js
|
||||
import Creem from 'creem';
|
||||
|
||||
const creem = new Creem({
|
||||
apiKey: process.env.CREEM_API_KEY
|
||||
});
|
||||
|
||||
// Create checkout
|
||||
const session = await creem.checkout.sessions.create({
|
||||
product_id: 'prod_xxx',
|
||||
success_url: 'https://example.com/success'
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# Python
|
||||
from creem import Creem
|
||||
|
||||
creem = Creem(api_key=os.environ['CREEM_API_KEY'])
|
||||
|
||||
session = creem.checkout.sessions.create(
|
||||
product_id='prod_xxx',
|
||||
success_url='https://example.com/success'
|
||||
)
|
||||
```
|
||||
|
||||
### Wrapper SDK (`creem_io`)
|
||||
|
||||
Helper functions for common operations:
|
||||
|
||||
```bash
|
||||
npm install creem_io
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { CreemClient, verifyWebhook } from 'creem_io';
|
||||
|
||||
const client = new CreemClient({
|
||||
apiKey: process.env.CREEM_API_KEY,
|
||||
webhookSecret: process.env.CREEM_WEBHOOK_SECRET
|
||||
});
|
||||
|
||||
// Simplified webhook verification
|
||||
app.post('/webhook', async (req, res) => {
|
||||
const event = client.verifyWebhook(req.body, req.headers['x-creem-signature']);
|
||||
// Handle event...
|
||||
});
|
||||
|
||||
// Access management helpers
|
||||
const hasAccess = await client.checkAccess(customerId, productId);
|
||||
```
|
||||
|
||||
## Framework Adapters
|
||||
|
||||
### Next.js Adapter
|
||||
|
||||
End-to-end billing integration:
|
||||
|
||||
```bash
|
||||
npm install @creem/nextjs
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app/api/checkout/route.ts
|
||||
import { createCheckout } from '@creem/nextjs';
|
||||
|
||||
export const POST = createCheckout({
|
||||
productId: 'prod_xxx',
|
||||
successUrl: '/success',
|
||||
cancelUrl: '/pricing'
|
||||
});
|
||||
|
||||
// app/api/webhooks/creem/route.ts
|
||||
import { handleWebhook } from '@creem/nextjs';
|
||||
|
||||
export const POST = handleWebhook({
|
||||
onCheckoutCompleted: async (session) => {
|
||||
await grantAccess(session.customer_id);
|
||||
},
|
||||
onSubscriptionCancelled: async (subscription) => {
|
||||
await revokeAccess(subscription.customer_id);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Better Auth Integration
|
||||
|
||||
Combined auth + payments:
|
||||
|
||||
```bash
|
||||
npm install @creem/better-auth
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { creemPlugin } from '@creem/better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
creemPlugin({
|
||||
apiKey: process.env.CREEM_API_KEY,
|
||||
webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
|
||||
products: {
|
||||
pro: 'prod_xxx',
|
||||
enterprise: 'prod_yyy'
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Check subscription in auth session
|
||||
const session = await auth.getSession();
|
||||
if (session.user.subscription?.status === 'active') {
|
||||
// User has active subscription
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js Template
|
||||
|
||||
Pre-built starter with Prisma, shadcn/ui, Tailwind:
|
||||
|
||||
```bash
|
||||
npx create-creem-app my-saas
|
||||
# or
|
||||
git clone https://github.com/creem-io/nextjs-template
|
||||
```
|
||||
|
||||
Includes:
|
||||
- Auth (Better Auth)
|
||||
- Database (Prisma)
|
||||
- UI (shadcn/ui, Tailwind)
|
||||
- Pricing page
|
||||
- Customer portal
|
||||
- Webhook handling
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
CREEM_API_KEY=sk_live_xxx # or sk_test_xxx for test mode
|
||||
CREEM_WEBHOOK_SECRET=whsec_xxx
|
||||
```
|
||||
|
||||
## AI Tool Integration
|
||||
|
||||
Creem supports Claude Code, Cursor, Windsurf via official skill - this document is part of that integration.
|
||||
@@ -0,0 +1,129 @@
|
||||
# Creem.io Subscriptions
|
||||
|
||||
## Subscription Lifecycle
|
||||
|
||||
```
|
||||
create → active → [pause] → [resume] → [upgrade] → cancel
|
||||
```
|
||||
|
||||
## Create Subscription
|
||||
|
||||
Via checkout session with recurring product:
|
||||
|
||||
```javascript
|
||||
const session = await creem.checkout.sessions.create({
|
||||
product_id: 'prod_recurring_xxx',
|
||||
success_url: 'https://example.com/success',
|
||||
customer_email: 'user@example.com'
|
||||
});
|
||||
```
|
||||
|
||||
## Retrieve Subscription
|
||||
|
||||
```javascript
|
||||
// GET /v1/subscriptions/:id
|
||||
const subscription = await creem.subscriptions.retrieve('sub_xxx');
|
||||
// Returns: { id, status, product_id, current_period_end, ... }
|
||||
```
|
||||
|
||||
## Modify Subscription
|
||||
|
||||
### Update Seats/Units
|
||||
|
||||
```javascript
|
||||
// PATCH /v1/subscriptions/:id
|
||||
const updated = await creem.subscriptions.update('sub_xxx', {
|
||||
quantity: 10, // Seat count
|
||||
prorate: true, // Prorate charges
|
||||
billing_immediately: false
|
||||
});
|
||||
```
|
||||
|
||||
### Upgrade/Downgrade
|
||||
|
||||
```javascript
|
||||
const updated = await creem.subscriptions.update('sub_xxx', {
|
||||
product_id: 'prod_higher_tier',
|
||||
prorate: true
|
||||
});
|
||||
```
|
||||
|
||||
## Pause Subscription
|
||||
|
||||
```javascript
|
||||
// POST /v1/subscriptions/:id/pause
|
||||
const paused = await creem.subscriptions.pause('sub_xxx', {
|
||||
resume_at: '2024-02-01T00:00:00Z' // Optional auto-resume date
|
||||
});
|
||||
```
|
||||
|
||||
## Resume Subscription
|
||||
|
||||
```javascript
|
||||
// POST /v1/subscriptions/:id/resume
|
||||
const resumed = await creem.subscriptions.resume('sub_xxx');
|
||||
```
|
||||
|
||||
## Cancel Subscription
|
||||
|
||||
```javascript
|
||||
// POST /v1/subscriptions/:id/cancel
|
||||
const cancelled = await creem.subscriptions.cancel('sub_xxx', {
|
||||
at_period_end: true // false = immediate cancellation
|
||||
});
|
||||
```
|
||||
|
||||
## Free Trials
|
||||
|
||||
Configure on product level:
|
||||
|
||||
```javascript
|
||||
const product = await creem.products.create({
|
||||
name: 'Pro Plan',
|
||||
price: 2900,
|
||||
currency: 'usd',
|
||||
recurring: { interval: 'month' },
|
||||
trial_period_days: 14
|
||||
});
|
||||
```
|
||||
|
||||
## Seat-Based Billing
|
||||
|
||||
```javascript
|
||||
const product = await creem.products.create({
|
||||
name: 'Team Plan',
|
||||
price: 1000, // Per seat price
|
||||
currency: 'usd',
|
||||
recurring: { interval: 'month' },
|
||||
billing_scheme: 'per_unit'
|
||||
});
|
||||
|
||||
// Checkout with quantity
|
||||
const session = await creem.checkout.sessions.create({
|
||||
product_id: 'prod_xxx',
|
||||
quantity: 5, // 5 seats
|
||||
success_url: '...'
|
||||
});
|
||||
```
|
||||
|
||||
## Product Bundles
|
||||
|
||||
Group related tiers for upsells:
|
||||
|
||||
```javascript
|
||||
const bundle = await creem.bundles.create({
|
||||
name: 'Growth Plans',
|
||||
products: ['prod_starter', 'prod_pro', 'prod_enterprise']
|
||||
});
|
||||
```
|
||||
|
||||
## Subscription Events (Webhooks)
|
||||
|
||||
- `subscription.created` - New subscription started
|
||||
- `subscription.updated` - Quantity, product, or status changed
|
||||
- `subscription.paused` - Subscription paused
|
||||
- `subscription.resumed` - Subscription resumed
|
||||
- `subscription.cancelled` - Cancellation scheduled or completed
|
||||
- `subscription.renewed` - Successful renewal charge
|
||||
|
||||
See `references/creem/webhooks.md` for webhook handling.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Creem.io Webhooks
|
||||
|
||||
## Webhook Setup
|
||||
|
||||
Configure webhook endpoint in Creem dashboard. Receive events at your endpoint URL.
|
||||
|
||||
## Event Types
|
||||
|
||||
### Checkout Events
|
||||
- `checkout.completed` - Payment successful, access granted
|
||||
- `checkout.abandoned` - Cart abandoned (triggers recovery emails if enabled)
|
||||
|
||||
### Subscription Events
|
||||
- `subscription.created` - New subscription started
|
||||
- `subscription.updated` - Changes to quantity, product, status
|
||||
- `subscription.paused` - Subscription paused
|
||||
- `subscription.resumed` - Subscription resumed
|
||||
- `subscription.cancelled` - Cancellation scheduled/completed
|
||||
- `subscription.renewed` - Successful renewal charge
|
||||
|
||||
### Payment Events
|
||||
- `payment.succeeded` - Charge successful
|
||||
- `payment.failed` - Charge failed
|
||||
- `refund.created` - Refund processed
|
||||
- `chargeback.created` - Dispute opened
|
||||
|
||||
### License Events
|
||||
- `license.activated` - Device activated against license
|
||||
- `license.deactivated` - Device deactivated
|
||||
|
||||
## Webhook Payload Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "evt_xxx",
|
||||
"type": "checkout.completed",
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"data": {
|
||||
"object": {
|
||||
"id": "cs_xxx",
|
||||
"customer_id": "cus_xxx",
|
||||
"product_id": "prod_xxx",
|
||||
"amount": 2900,
|
||||
"currency": "usd",
|
||||
"metadata": { "order_id": "123" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Signature Verification
|
||||
|
||||
```javascript
|
||||
import crypto from 'crypto';
|
||||
|
||||
function verifyWebhook(payload, signature, secret) {
|
||||
const expected = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload, 'utf8')
|
||||
.digest('hex');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expected)
|
||||
);
|
||||
}
|
||||
|
||||
// Express handler
|
||||
app.post('/webhooks/creem', express.raw({ type: 'application/json' }), (req, res) => {
|
||||
const signature = req.headers['x-creem-signature'];
|
||||
const payload = req.body.toString();
|
||||
|
||||
if (!verifyWebhook(payload, signature, process.env.CREEM_WEBHOOK_SECRET)) {
|
||||
return res.status(401).send('Invalid signature');
|
||||
}
|
||||
|
||||
const event = JSON.parse(payload);
|
||||
|
||||
switch (event.type) {
|
||||
case 'checkout.completed':
|
||||
await handleCheckoutCompleted(event.data.object);
|
||||
break;
|
||||
case 'subscription.cancelled':
|
||||
await handleSubscriptionCancelled(event.data.object);
|
||||
break;
|
||||
}
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
## Idempotency
|
||||
|
||||
Store processed event IDs to prevent duplicate processing:
|
||||
|
||||
```javascript
|
||||
async function handleWebhook(event) {
|
||||
// Check if already processed
|
||||
const existing = await db.webhookEvents.findOne({ eventId: event.id });
|
||||
if (existing) return { status: 'already_processed' };
|
||||
|
||||
// Process event
|
||||
await processEvent(event);
|
||||
|
||||
// Mark as processed
|
||||
await db.webhookEvents.create({
|
||||
eventId: event.id,
|
||||
type: event.type,
|
||||
processedAt: new Date()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Retry Behavior
|
||||
|
||||
Creem retries failed webhooks (non-2xx responses). Implement idempotency to handle retries safely.
|
||||
|
||||
## Testing Webhooks
|
||||
|
||||
Use test mode API keys (`sk_test_`) - events sent to same webhook endpoint with test data.
|
||||
Reference in New Issue
Block a user