🎓 Invite Student to Course API
This API endpoint allows you to invite students to courses using API token authentication. It creates new user accounts for non-existing users and adds existing users to the specified course.
📍 Endpoint
🔐 Authentication
This endpoint requires API token authentication. You must include a valid API token in the Authorization header.
Header Format:
Authorization: Bearer <your-api-token>📝 Request Parameters
Request Body (JSON):
| Parameter | Type | Required | Description |
|---|---|---|---|
| schoolId | string | ✅ Yes | The unique identifier of the school |
| productId | string | ✅ Yes | The unique identifier of the course/product |
| string | ✅ Yes | Valid email address of the student to invite. Exactly one email per request | |
| accessMonths new | number | ❌ No | How many months of access to grant. Defaults to 6; values of 0 or less fall back to 6. Determines the expiry date and how many invitations are charged — see Access Period |
Example Request:
POST /inviteStudentToCourse
Content-Type: application/json
Authorization: Bearer your-api-token-here
{
"schoolId": "school123",
"productId": "course456",
"email": "student@example.com",
"accessMonths": 12
}📅 Access Period & Invitation Cost
How the access window is calculated
Access is granted as a duration, not as a calendar date. On success the student's expiry (paidUntil) is always calculated from the moment the request is processed:
paidUntil = <time of the API call> + accessMonthsMonths are added as calendar months, so a 6-month invitation created on 15 January expires on 15 July.
- There is no request field for an absolute expiry date or an access start date.
- The invitation email is sent immediately during the request — it cannot be scheduled for a later date.
- To grant access until a specific date, calculate the number of whole months between today and that date and pass it as
accessMonths. This rounds to whole months.
paidUntil. Extending past paidUntil requires purchasing more access.
Invitation cost
Each newly enrolled student consumes invitations from your school's balance. The amount depends on accessMonths and on whether your school is on custom pricing:
| Pricing | Invitations charged per student |
|---|---|
| Standard | ceil(accessMonths / 6) — 6 months per invitation |
| Custom pricing | ceil(accessMonths / 12) — 12 months per invitation |
Examples on standard pricing:
accessMonths | Invitations charged |
|---|---|
| omitted (defaults to 6) | 1 |
| 6 | 1 |
| 12 | 2 |
| 18 | 3 |
| 24 | 4 |
📤 Response Format
Success Response (200):
{
"success": true,
"brokenEmails": []
}success is true only when the student was actually added to the course. Every other outcome returns HTTP 400.
Error Response:
{
"success": false,
"error": "Error description",
"required": 2, // Only for insufficient balance: total cost of the request
"available": 1, // Only for insufficient balance: current balance
"invitationsPerUser": 2, // Only for insufficient balance: cost implied by accessMonths
"customPricing": false // Only for insufficient balance: which cost formula applied
}📊 Status Codes
| Status Code | Description | Common Causes |
|---|---|---|
| 200 | Success | Student successfully invited to course |
| 400 | Bad Request | Missing required fields, invalid email format, material product, missing billing record, insufficient invitation balance, or the student could not be enrolled |
| 401 | Unauthorized | Missing, invalid, or inactive API token |
| 403 | Forbidden | Product doesn't belong to the specified school, or the product is a shared material (shm- prefix) |
| 404 | Not Found | Product not found |
| 405 | Method Not Allowed | Using HTTP method other than POST |
| 429 | Too Many Requests | More than 10 requests at the same time; wait a moment and retry |
| 500 | Internal Server Error | Server error during processing |
⚡ Features
- Automatic User Creation: Creates new Firebase Auth users for non-existing emails
- Email Notifications: Sends welcome emails with login credentials
- Multi-language Support: Supports Ukrainian, English, Russian, and Polish emails
- Configurable Access Period: Set the access duration per invitation via
accessMonths - Invitation Balance: Deducted in a transaction and refunded automatically on failure
- Duplicate Prevention: Prevents adding users already in the course
- Role Management: Automatically assigns student role
🚫 Limitations
- Only supports inviting students (role is fixed)
- Cannot invite students to material products or shared materials
- Access is expressed in whole months only — no absolute expiry date, no scheduled sending
- Requires sufficient invitation balance in school billing for the requested
accessMonths - One email per request (for bulk invitations, use multiple requests)
- At most 10 requests at the same time: send bulk invitations one after another or a few at a time
- API token must be active and belong to the specified school
📋 Example Usage
JavaScript/Node.js:
const inviteStudent = async (schoolId, productId, email, apiToken, accessMonths) => {
try {
const response = await fetch('https://invitestudenttocourse-s4mogebb3q-lm.a.run.app', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
},
body: JSON.stringify({
schoolId: schoolId,
productId: productId,
email: email,
// Optional. Omit to use the 6-month default.
// Must be a number, not a string.
...(accessMonths ? { accessMonths: Number(accessMonths) } : {})
})
});
const result = await response.json();
if (response.ok && result.success) {
console.log('Student invited successfully!');
return result;
} else {
console.error('Failed to invite student:', result.error || result.brokenEmails);
throw new Error(result.error || 'Student could not be enrolled');
}
} catch (error) {
console.error('Request failed:', error);
throw error;
}
};
// Usage
inviteStudent('school123', 'course456', 'student@example.com', 'your-api-token', 12)
.then(result => console.log('Success:', result))
.catch(error => console.error('Error:', error));Python:
import requests
def invite_student(school_id, product_id, email, api_token, access_months=None):
url = "https://invitestudenttocourse-s4mogebb3q-lm.a.run.app"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_token}"
}
data = {
"schoolId": school_id,
"productId": product_id,
"email": email
}
# Optional. Omit to use the 6-month default. Must be an int, not a string.
if access_months is not None:
data["accessMonths"] = int(access_months)
try:
response = requests.post(url, headers=headers, json=data)
result = response.json()
if response.status_code == 200 and result.get('success'):
print("Student invited successfully!")
return result
error_msg = result.get('error') or f"Not enrolled: {result.get('brokenEmails')}"
print(f"Failed to invite student: {error_msg}")
raise Exception(error_msg)
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
raise
# Usage
try:
result = invite_student('school123', 'course456', 'student@example.com', 'your-api-token', 12)
print("Success:", result)
except Exception as e:
print("Error:", e)cURL:
curl -X POST \
https://invitestudenttocourse-s4mogebb3q-lm.a.run.app \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer your-api-token' \
-d '{
"schoolId": "school123",
"productId": "course456",
"email": "student@example.com",
"accessMonths": 12
}'🔧 Error Handling Examples
Invalid Email Format:
// Request
{
"schoolId": "school123",
"productId": "course456",
"email": "invalid-email"
}
// Response (400)
{
"success": false,
"error": "Invalid email format"
}Insufficient Invitation Balance:
// Request — 12 months costs 2 invitations on standard pricing
{
"schoolId": "school123",
"productId": "course456",
"email": "student@example.com",
"accessMonths": 12
}
// Response (400)
{
"success": false,
"error": "Not enough invitation balance",
"required": 2,
"available": 1,
"invitationsPerUser": 2,
"customPricing": false
}Shared Material Product:
// Request
{
"schoolId": "school123",
"productId": "shm-abc123",
"email": "student@example.com"
}
// Response (403)
{
"success": false,
"error": "Cannot invite students to shared materials via API"
}Student Already in Course:
// Response (400)
{
"success": false,
"brokenEmails": ["student@example.com"]
}Email Belongs to a Non-Student Account:
// Response (400) — the account already exists as a teacher or admin.
// It is left untouched and is NOT listed in brokenEmails.
{
"success": false,
"brokenEmails": []
}- Always validate email format on the client side before sending requests
- Check the
successfield, not just the status code — a 400 may carrybrokenEmailsinstead oferror - Send
accessMonthsas a JSON number, never as a quoted string - Implement proper error handling for all possible status codes
- Store API tokens securely and never expose them in client-side code
- Monitor your invitation balance — a larger
accessMonthscosts more invitations - Use HTTPS for all API requests