Quick Navigation
Grant Course Access
Automatically deliver digital products
Manage Subscriptions
Handle recurring access and cancellations
Sync to CRM
Keep customer data synchronized
Custom Checkout
Build your own checkout experience
Email Automation
Trigger emails based on events
Discord Integration
Automatically assign Discord roles
Grant Course Access on Purchase
Overview
Automatically grant access to your course platform when a customer completes a purchase. What you’ll need:- Pocketsflow webhook configured
- Course platform with API (Teachable, Thinkific, custom LMS, etc.)
- Server to receive webhooks
Step 1: Set up webhook endpoint
// server.js
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.POCKETSFLOW_WEBHOOK_SECRET;
app.post('/webhooks/pocketsflow',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Verify signature
const signature = req.headers['x-pocketsflow-signature'];
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature');
}
// Parse event
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).send('OK');
// Process asynchronously
try {
await handleEvent(event);
} catch (error) {
console.error('Error processing webhook:', error);
// Store for retry
await storeFailedWebhook(event, error);
}
}
);
Step 2: Handle order completion
async function handleEvent(event) {
if (event.event !== 'order.completed') {
return; // Ignore other events
}
const { customer, product, order } = event;
// Check if already processed (idempotency)
const existingGrant = await db.courseAccess.findOne({
orderId: order.id
});
if (existingGrant) {
console.log('Access already granted for order:', order.id);
return;
}
// Map product ID to course ID
const courseId = await getCourseIdForProduct(product.id);
if (!courseId) {
console.warn('No course mapped for product:', product.id);
return;
}
// Grant access to your course platform
await grantCourseAccess({
email: customer.email,
firstName: customer.firstName,
lastName: customer.lastName,
courseId: courseId,
orderId: order.id
});
// Send welcome email
await sendWelcomeEmail(customer.email, courseId);
// Log successful grant
await db.courseAccess.create({
orderId: order.id,
customerId: customer.email,
courseId: courseId,
grantedAt: new Date()
});
}
Step 3: Integrate with your course platform
For Teachable:async function grantCourseAccess({ email, firstName, lastName, courseId }) {
const response = await fetch('https://teachable.com/api/v1/users/enroll', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TEACHABLE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
first_name: firstName,
last_name: lastName,
course_id: courseId
})
});
if (!response.ok) {
throw new Error(`Teachable API error: ${response.statusText}`);
}
return await response.json();
}
async function grantCourseAccess({ email, courseId, orderId }) {
// Find or create user
let user = await db.users.findOne({ email });
if (!user) {
user = await db.users.create({
email,
createdAt: new Date()
});
}
// Grant course access
await db.enrollments.create({
userId: user.id,
courseId,
orderId,
enrolledAt: new Date(),
expiresAt: null // Lifetime access
});
// Send login credentials if new user
if (!user.hasLoggedIn) {
await sendLoginCredentials(email);
}
}
Step 4: Handle refunds
async function handleEvent(event) {
// ... existing code ...
if (event.event === 'order.refunded') {
const { order } = event;
// Revoke access
await db.enrollments.updateMany(
{ orderId: order.id },
{ revokedAt: new Date(), active: false }
);
console.log('Access revoked for refunded order:', order.id);
}
}
Complete example
See the full production-ready implementation:// Complete webhook handler with retry logic
const express = require('express');
const crypto = require('crypto');
const Queue = require('bull');
const app = express();
const webhookQueue = new Queue('webhooks', process.env.REDIS_URL);
// Webhook endpoint
app.post('/webhooks/pocketsflow',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['x-pocketsflow-signature'];
const expectedSignature = crypto
.createHmac('sha256', process.env.POCKETSFLOW_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
res.status(200).send('OK');
// Queue for background processing
await webhookQueue.add(event, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});
}
);
// Process webhooks in background
webhookQueue.process(async (job) => {
const event = job.data;
switch (event.event) {
case 'order.completed':
await handleOrderCompleted(event);
break;
case 'order.refunded':
await handleOrderRefunded(event);
break;
}
});
async function handleOrderCompleted(event) {
const { customer, product, order } = event;
// Idempotency check
if (await db.grants.exists({ orderId: order.id })) {
return;
}
const courseId = PRODUCT_COURSE_MAP[product.id];
if (!courseId) {
throw new Error(`No course for product ${product.id}`);
}
// Grant access
await grantCourseAccess({
email: customer.email,
name: `${customer.firstName} ${customer.lastName}`,
courseId
});
// Record grant
await db.grants.create({
orderId: order.id,
email: customer.email,
courseId,
grantedAt: new Date()
});
// Send welcome email
await sendEmail({
to: customer.email,
template: 'course-access',
data: { courseId, loginUrl: process.env.COURSE_LOGIN_URL }
});
}
app.listen(3000, () => console.log('Server running on port 3000'));
Manage Subscription Access
Overview
Automatically grant and revoke access based on subscription status (active, canceled, payment failed).Step 1: Handle subscription events
async function handleSubscriptionEvent(event) {
const { subscription, customer } = event;
switch (event.event) {
case 'customer.subscription.created':
case 'invoice.payment_succeeded':
await grantMembershipAccess(customer.email);
break;
case 'customer.subscription.deleted':
case 'invoice.payment_failed':
await revokeMembershipAccess(customer.email);
break;
case 'customer.subscription.trial_will_end':
await sendTrialEndingReminder(customer.email, 3);
break;
}
}
Step 2: Grant membership access
async function grantMembershipAccess(email) {
// Update member status
await db.members.upsert({
email,
status: 'active',
accessGrantedAt: new Date()
});
// Send welcome email
await sendEmail({
to: email,
template: 'membership-welcome',
data: {
loginUrl: process.env.MEMBER_PORTAL_URL,
supportEmail: 'support@yourdomain.com'
}
});
// Sync to external platform (Discord, Circle, etc.)
await syncMemberToExternalPlatform(email, 'active');
}
Step 3: Handle payment failures
async function handlePaymentFailure(event) {
const { customer, invoice } = event;
// Grace period: 3 failed attempts before revoking
const failedAttempts = invoice.attemptCount;
if (failedAttempts === 1) {
// First failure: Send payment update request
await sendEmail({
to: customer.email,
template: 'payment-failed-update-method',
data: {
updatePaymentUrl: `${process.env.APP_URL}/update-payment`,
invoiceAmount: invoice.amountDue
}
});
} else if (failedAttempts === 3) {
// Third failure: Revoke access
await revokeMembershipAccess(customer.email);
await sendEmail({
to: customer.email,
template: 'access-revoked-payment-failure',
data: {
reactivateUrl: `${process.env.APP_URL}/reactivate`
}
});
}
// Log for monitoring
await db.paymentFailures.create({
email: customer.email,
attemptCount: failedAttempts,
invoiceId: invoice.id,
failedAt: new Date()
});
}
Step 4: Revoke access
async function revokeMembershipAccess(email) {
// Update member status
await db.members.update(
{ email },
{
status: 'inactive',
accessRevokedAt: new Date()
}
);
// Remove from external platforms
await syncMemberToExternalPlatform(email, 'inactive');
console.log('Access revoked for:', email);
}
Sync Customers to CRM
Overview
Automatically sync customer data to your CRM (HubSpot, Salesforce, Pipedrive, etc.) when they make a purchase.HubSpot integration
const hubspot = require('@hubspot/api-client');
const hubspotClient = new hubspot.Client({ accessToken: process.env.HUBSPOT_API_KEY });
async function syncToHubSpot(event) {
if (event.event !== 'customer.created' && event.event !== 'order.completed') {
return;
}
const { customer, product, amount } = event;
try {
// Create or update contact
const contactData = {
properties: {
email: customer.email,
firstname: customer.firstName,
lastname: customer.lastName,
country: customer.country || '',
lifecyclestage: 'customer',
hs_lead_status: 'OPEN'
}
};
let contact;
try {
// Try to update existing
contact = await hubspotClient.crm.contacts.basicApi.update(
customer.email,
contactData
);
} catch (err) {
// Create new contact
contact = await hubspotClient.crm.contacts.basicApi.create(contactData);
}
// Create deal if order completed
if (event.event === 'order.completed') {
await hubspotClient.crm.deals.basicApi.create({
properties: {
dealname: `${product.name} - ${customer.email}`,
amount: amount,
dealstage: 'closedwon',
pipeline: 'default',
closedate: new Date().toISOString()
},
associations: [
{
to: { id: contact.id },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }]
}
]
});
}
console.log('Synced to HubSpot:', customer.email);
} catch (error) {
console.error('HubSpot sync error:', error);
throw error;
}
}
Salesforce integration
const jsforce = require('jsforce');
async function syncToSalesforce(event) {
const conn = new jsforce.Connection({
loginUrl: process.env.SALESFORCE_LOGIN_URL
});
await conn.login(
process.env.SALESFORCE_USERNAME,
process.env.SALESFORCE_PASSWORD + process.env.SALESFORCE_SECURITY_TOKEN
);
const { customer, product, amount } = event;
// Create or update contact
const contactResult = await conn.sobject('Contact').upsert({
Email: customer.email,
FirstName: customer.firstName,
LastName: customer.lastName,
LeadSource: 'Pocketsflow'
}, 'Email');
// Create opportunity
if (event.event === 'order.completed') {
await conn.sobject('Opportunity').create({
Name: `${product.name} - ${customer.email}`,
StageName: 'Closed Won',
CloseDate: new Date().toISOString().split('T')[0],
Amount: amount,
ContactId: contactResult.id
});
}
}
Build Custom Checkout Flow
Overview
Create a custom checkout experience using the Pocketsflow API for payment processing.Step 1: Create payment intent
// Client-side
async function createCheckout(productId, email) {
const response = await fetch('/api/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, email })
});
const { clientSecret, paymentIntentId } = await response.json();
return { clientSecret, paymentIntentId };
}
// Server-side
app.post('/api/create-checkout', async (req, res) => {
const { productId, email } = req.body;
// Fetch product details from Pocketsflow
const product = await fetch(`https://api.pocketsflow.com/products/${productId}`, {
headers: { 'Authorization': `Bearer ${process.env.POCKETSFLOW_API_KEY}` }
}).then(r => r.json());
// Create payment intent
const paymentIntent = await fetch('https://api.pocketsflow.com/products/create-payment-intent', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.POCKETSFLOW_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
productId,
currency: product.currency || 'usd',
webhookMetadata: {
customField: 'value',
email
}
})
}).then(r => r.json());
res.json({
clientSecret: paymentIntent.clientSecret,
paymentIntentId: paymentIntent.paymentIntentId
});
});
Step 2: Build checkout UI
// Example using a generic payment gateway SDK
import { PaymentGateway } from 'payment-gateway-sdk';
import { PaymentForm, PaymentProvider } from 'payment-gateway-react';
const paymentGateway = new PaymentGateway('pk_live_...');
function CheckoutForm({ clientSecret }) {
const payment = usePayment();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setProcessing(true);
const { error: submitError } = await payment.confirmPayment({
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
});
if (submitError) {
setError(submitError.message);
setProcessing(false);
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentForm />
<button disabled={!payment || processing}>
{processing ? 'Processing...' : 'Pay Now'}
</button>
{error && <div className="error">{error}</div>}
</form>
);
}
function Checkout({ productId, email }) {
const [clientSecret, setClientSecret] = useState('');
useEffect(() => {
createCheckout(productId, email).then(({ clientSecret }) => {
setClientSecret(clientSecret);
});
}, []);
return (
<PaymentProvider gateway={paymentGateway} options={{ clientSecret }}>
<CheckoutForm clientSecret={clientSecret} />
</PaymentProvider>
);
}
Send Custom Emails on Events
Overview
Trigger custom emails using your email service (SendGrid, Mailgun, Resend) based on Pocketsflow events.Using Resend
const { Resend } = require('resend');
const resend = new Resend(process.env.RESEND_API_KEY);
async function sendCustomEmail(event) {
const { customer, product, order } = event;
let template, subject, data;
switch (event.event) {
case 'order.completed':
template = 'purchase-confirmation';
subject = `Your ${product.name} is ready!`;
data = {
productName: product.name,
customerName: customer.firstName,
downloadUrl: `${process.env.APP_URL}/download/${order.id}`,
supportEmail: 'support@yourdomain.com'
};
break;
case 'customer.subscription.trial_will_end':
template = 'trial-ending';
subject = 'Your trial ends in 3 days';
data = {
customerName: customer.firstName,
trialEndDate: event.subscription.trialEnd,
upgradeUrl: `${process.env.APP_URL}/upgrade`
};
break;
default:
return;
}
await resend.emails.send({
from: 'noreply@yourdomain.com',
to: customer.email,
subject,
react: EmailTemplate({ template, data })
});
}
Grant Discord Roles
Overview
Automatically assign Discord roles when customers purchase or subscribe.Step 1: Discord bot setup
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers]
});
client.login(process.env.DISCORD_BOT_TOKEN);
async function grantDiscordRole(email, roleId) {
const guild = await client.guilds.fetch(process.env.DISCORD_GUILD_ID);
// Find member by email (requires members to link Discord)
const member = await findMemberByEmail(guild, email);
if (!member) {
console.log('Discord member not found for:', email);
return;
}
const role = await guild.roles.fetch(roleId);
await member.roles.add(role);
console.log('Discord role granted:', email, role.name);
}
Step 2: Handle webhook
async function handleDiscordIntegration(event) {
if (event.event === 'order.completed') {
const roleId = PRODUCT_ROLE_MAP[event.product.id];
if (roleId) {
await grantDiscordRole(event.customer.email, roleId);
}
}
if (event.event === 'order.refunded') {
const roleId = PRODUCT_ROLE_MAP[event.product.id];
if (roleId) {
await revokeDiscordRole(event.customer.email, roleId);
}
}
}
Related Topics
Webhook Events
See all available webhook events
API Reference
Full API documentation
Webhook Security
Secure your webhooks
Consuming Webhooks
Best practices for webhooks