Skip to main content

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();
}
For custom LMS:
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);
    }
  }
}

Webhook Events

See all available webhook events

API Reference

Full API documentation

Webhook Security

Secure your webhooks

Consuming Webhooks

Best practices for webhooks