Webhooks allow you to receive real-time HTTP notifications when events occur in your account, eliminating the need for polling.
Available Events
Course Events
course.created- New course createdcourse.updated- Course details modifiedcourse.published- Course published/unpublishedcourse.deleted- Course deleted
Student Events
student.enrolled- Student enrolled in coursestudent.completed_lesson- Student completed a lessonstudent.completed_course- Student completed entire coursestudent.unenrolled- Student unenrolled from course
Module Events
module.created- New module addedmodule.updated- Module modifiedmodule.deleted- Module removed
Lesson Events
lesson.created- New lesson addedlesson.updated- Lesson modifiedlesson.deleted- Lesson removed
Assessment Events
quiz.submitted- Student submitted quizquiz.graded- Quiz graded (auto or manual)
Setting Up Webhooks
1. Create a Webhook Endpoint
Your endpoint must:
- Accept POST requests
- Respond with 200 OK within 5 seconds
- Handle duplicate events (idempotent)
Example endpoint (Node.js/Express):
app.post('/webhooks/courseforge', (req, res) => {
const event = req.body
// Verify signature (recommended)
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature')
}
// Process event
console.log('Received event:', event.type)
// Respond quickly
res.status(200).json({ received: true })
// Process async
processEvent(event).catch(console.error)
})
2. Register Webhook
Register your webhook URL in the CourseForge dashboard:
- Go to Settings → Webhooks
- Click Add Webhook
- Enter your endpoint URL
- Select events to subscribe to
- Click Create
You'll receive a webhook secret - save this securely.
Webhook Payload
All webhook requests include:
{
"id": "evt_abc123",
"type": "course.created",
"created": "2025-01-15T10:30:00Z",
"data": {
"course": {
"id": "course_123",
"name": "Introduction to Python",
"authorId": "user_456"
}
},
"account": {
"id": "user_456",
"email": "user@example.com"
}
}
Verifying Signatures
Verify webhook authenticity using the signature header:
const crypto = require('crypto')
function verifySignature(req) {
const signature = req.headers['x-courseforge-signature']
const payload = JSON.stringify(req.body)
const secret = process.env.WEBHOOK_SECRET
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
Retry Logic
If your endpoint fails (non-200 response), CourseForge will retry:
- 1st retry: After 1 minute
- 2nd retry: After 5 minutes
- 3rd retry: After 30 minutes
- 4th retry: After 2 hours
- 5th retry: After 6 hours
After 5 failures, the webhook is automatically disabled.
Best Practices
Respond Quickly
- Return 200 OK immediately
- Process events asynchronously
- Use a message queue for complex processing
Handle Duplicates
- Store processed event IDs
- Check before processing
- Make operations idempotent
Monitor Webhooks
- Log all received events
- Set up error alerts
- Track processing times
Security
- Always verify signatures
- Use HTTPS endpoints only
- Validate event data
- Rate limit your endpoint
Testing Webhooks
Local Testing with ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# Create tunnel
ngrok http 3000
# Use the ngrok URL in webhook settings
https://abc123.ngrok.io/webhooks/courseforge
Manual Testing
Send test events from the dashboard:
- Go to Settings → Webhooks
- Click on your webhook
- Click Send Test Event
- Select event type
- View delivery logs
Troubleshooting
Webhook Not Receiving Events
- Check your endpoint is publicly accessible
- Verify the URL is correct (including https://)
- Check firewall/security rules
- View webhook logs in dashboard
Events Timing Out
- Respond within 5 seconds
- Move slow processing to background jobs
- Use a message queue
Duplicate Events
- This is normal - design for idempotency
- Track processed event IDs
- Use database constraints
Rate Limits
Webhooks are not subject to API rate limits, but your endpoint should handle:
- Burst traffic (many events at once)
- Retry attempts
- Reasonable response times
Example: Process Course Created
async function processEvent(event) {
if (event.type === 'course.created') {
const course = event.data.course
// Send welcome email
await sendEmail({
to: event.account.email,
subject: `Course "${course.name}" Created`,
body: 'Your course is ready!'
})
// Log to analytics
await analytics.track('course_created', {
courseId: course.id,
userId: event.account.id
})
// Trigger automation
await automation.run('new-course', course)
}
}