🎓 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

POST https://invitestudenttocourse-s4mogebb3q-lm.a.run.app

🔐 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>
Note: API tokens can be generated by school owners in the school settings. Navigate to your school settings page and look for "API Tokens" section to generate a new token.

📝 Request Parameters

Request Body (JSON):

ParameterTypeRequiredDescription
schoolIdstring✅ YesThe unique identifier of the school
productIdstring✅ YesThe unique identifier of the course/product
emailstring✅ YesValid email address of the student to invite. Exactly one email per request
accessMonths newnumber❌ NoHow 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> + accessMonths

Months are added as calendar months, so a 6-month invitation created on 15 January expires on 15 July.

⚠️ Setting an explicit date is not supported.
  • 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.
After enrollment a school administrator can fine-tune the expiry date from the dashboard (User Management → access date), but only to a date earlier than or equal to 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:

PricingInvitations charged per student
Standardceil(accessMonths / 6) — 6 months per invitation
Custom pricingceil(accessMonths / 12) — 12 months per invitation

Examples on standard pricing:

accessMonthsInvitations charged
omitted (defaults to 6)1
61
122
183
244
The balance is reserved before the student is processed and is automatically refunded if the enrollment does not complete. Students who are already enrolled in the course do not consume any invitations.

📤 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 CodeDescriptionCommon Causes
200SuccessStudent successfully invited to course
400Bad RequestMissing required fields, invalid email format, material product, missing billing record, insufficient invitation balance, or the student could not be enrolled
401UnauthorizedMissing, invalid, or inactive API token
403ForbiddenProduct doesn't belong to the specified school, or the product is a shared material (shm- prefix)
404Not FoundProduct not found
405Method Not AllowedUsing HTTP method other than POST
429Too Many RequestsMore than 10 requests at the same time; wait a moment and retry
500Internal Server ErrorServer 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": []
}
✅ Best Practices:
  • Always validate email format on the client side before sending requests
  • Check the success field, not just the status code — a 400 may carry brokenEmails instead of error
  • Send accessMonths as 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 accessMonths costs more invitations
  • Use HTTPS for all API requests