Skip to main content
osky
How to Detect Students with No Courses in SendPulse and Automate Enrollment from Your WhatsApp Bot

How to Detect Students with No Courses in SendPulse and Automate Enrollment from Your WhatsApp Bot

SendPulse recently rolled out the "No course" filter inside the Students tab of its Academy module. It sounds like a minor UI update, but for anyone managing hundreds or thousands of contacts through a WhatsApp bot that sells or distributes courses, this is exactly the hook you needed to close the loop: know who's sitting in your database without any active course, and act on it automatically.

Captura de pantalla estilizada del filtro No course en panel de Academy

This tutorial walks you through everything from the SendPulse dashboard configuration all the way to the code snippet that triggers enrollment and sends the welcome message over WhatsApp. No fluff.


Prerequisites

Before writing a single line of code, make sure you have the following in place:

  • A SendPulse account with the Academy module active (paid plan or trial with course access).
  • At least one published course in your Academy.
  • An active WhatsApp bot in SendPulse (or WhatsApp Cloud API connected to your backend).
  • SendPulse REST API credentials: client_id and client_secret (Settings → API).
  • Node.js 20+ or Python 3.11+ on your server or local environment for the code snippets.
  • ngrok or a server with a public IP if you need to receive webhooks during development.

Step 1 — Understand the "No course" Filter in the Dashboard

Go to Academy → Students. In the filter bar, select the No course status. SendPulse will display only the learners registered in your academy who have no course assigned.

This happens for several real-world production reasons:

  • The student registered through a lead capture form but never completed checkout.
  • You imported them manually from a CSV of leads.
  • They finished a course and you removed them from the previous one without reassigning them.

The dashboard lets you select them all and enroll or remove them in bulk. But if you want this to happen automatically when a contact replies to your bot, you need the API.


Step 2 — Get Your API Access Token

SendPulse uses OAuth 2.0 with client_credentials. Each token lasts 3600 seconds; cache it.

// auth.js — Node.js 20+
const getToken = async () => {
  const res = await fetch('https://api.sendpulse.com/oauth/access_token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'client_credentials',
      client_id: process.env.SP_CLIENT_ID,
      client_secret: process.env.SP_CLIENT_SECRET,
    }),
  });
  const data = await res.json();
  return data.access_token; // store in memory or Redis with 3500s TTL
};

Production warning: Never make an auth call on every single request. In high-volume bots you can hit the auth rate limit within minutes. Use a singleton or a cache with TTL.


Step 3 — Query Students with No Course via API

The endpoint for listing academy students accepts filter parameters:

// students.js
const getStudentsNoCourse = async (token, academyId) => {
  const url = `https://api.sendpulse.com/academy/${academyId}/students?status=no_course&limit=100&offset=0`;
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const data = await res.json();
  return data.data ?? []; // array of student objects
};

Each student object contains at minimum: id, email, phone, name, status.

Note: Verify the exact status parameter name in the current SendPulse Academy API docs, as it may vary between versions. As of this writing (June 2026) the documented value is no_course.


Step 4 — Enroll the Student in a Course

Once you have the student_id and course_id, the enrollment endpoint is a POST:

// enroll.js
const enrollStudent = async (token, academyId, courseId, studentId) => {
  const res = await fetch(
    `https://api.sendpulse.com/academy/${academyId}/courses/${courseId}/students`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ student_id: studentId }),
    }
  );
  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Enroll failed: ${JSON.stringify(err)}`);
  }
  return await res.json();
};

Step 5 — Connect This to Your WhatsApp Bot

Here's the meaty part. When a contact in your bot reaches a specific node (for example, they reply "YES" to "Do you want access to the free course?"), your webhook receives the event. At that point:

  1. Look up the contact by phone number in Academy.
  2. Check if they have status === 'no_course'.
  3. If yes, enroll them.
  4. Send the welcome message with the course link.
// webhook-handler.js
import express from 'express';
import { getToken } from './auth.js';
import { getStudentsNoCourse } from './students.js';
import { enrollStudent } from './enroll.js';

const app = express();
app.use(express.json());

const ACADEMY_ID = process.env.SP_ACADEMY_ID;
const COURSE_ID = process.env.SP_COURSE_ID;

app.post('/webhook/whatsapp', async (req, res) => {
  const { contact_phone, message_text } = req.body; // adapt to your actual structure
  res.sendStatus(200); // respond fast, process async

  if (message_text?.toLowerCase() !== 'yes') return;

  try {
    const token = await getToken();
    const students = await getStudentsNoCourse(token, ACADEMY_ID);
    const student = students.find(s => s.phone === contact_phone);

    if (!student) {
      console.log(`Contact ${contact_phone} is not in Academy yet.`);
      return;
    }

    await enrollStudent(token, ACADEMY_ID, COURSE_ID, student.id);

    // Send the WhatsApp message with the course link
    // using SendPulse Chatbots API or WhatsApp Cloud directly
    await sendWhatsAppMessage(contact_phone, token,
      `You're in! Access your course here: https://academy.yourdomain.com/courses/${COURSE_ID}`
    );
  } catch (e) {
    console.error('Enrollment flow error:', e);
  }
});

app.listen(3000);

Critical warning: Respond to the webhook with 200 before processing any logic. SendPulse (and most messaging platforms) will retry the webhook if it doesn't get a response within ~5 seconds. If your logic takes longer, you'll end up enrolling the same student two or three times.


Step 6 — Automate the Nightly Sweep (Cron Job)

On top of the reactive flow, it's worth having a job that every night checks all students with no_course and sends them a re-engagement message over WhatsApp:

// cron.js — use node-cron or a system cronjob
import cron from 'node-cron';
import { getToken } from './auth.js';
import { getStudentsNoCourse } from './students.js';

cron.schedule('0 9 * * *', async () => { // every day at 9am
  const token = await getToken();
  const students = await getStudentsNoCourse(token, process.env.SP_ACADEMY_ID);
  console.log(`Students with no course: ${students.length}`);
  for (const s of students) {
    // re-engagement logic: send message or create CRM task
    console.log(`Pending re-engagement: ${s.email} / ${s.phone}`);
  }
});

Final Production Considerations

  • Idempotency: Before enrolling, verify the student isn't already in the course to avoid duplicates if the webhook fires twice.
  • Rate limits: The SendPulse API has per-minute limits. If you have more than 500 students with no course, paginate the query using offset and add a sleep between bulk enrollments.
  • Logging: Save every enrollment to your database with a timestamp and status. If something breaks at 3am, you want to know exactly what happened.
  • Environment variables: Never hardcode client_id or client_secret. Use .env + dotenv or your deployment platform's secrets manager.

With this you have the complete flow: from the visual filter in the dashboard to the bot that detects, enrolls, and notifies without you lifting a finger.