API — Event ingestion
The ingest API receives learning events from Moodle (or other sources) and queues them for processing. It is the surface used by the Moodle plugin, but you can send events directly if you build your own integrator.
Base URL
Section titled “Base URL”https://ingest.softsysanalytics.comAuthentication
Section titled “Authentication”All ingest endpoints (except /health) require the header:
X-SSEA-Key: ssea_<32_hex_chars>Keys are provisioned by SoftSys Solutions per tenant. See Security & privacy for details on format, expiry, and revocation.
Endpoints
Section titled “Endpoints”POST /v1/events — Send one event
Section titled “POST /v1/events — Send one event”Sends a single event. Useful for low-volume integrators or tests.
curl -X POST https://ingest.softsysanalytics.com/v1/events \ -H "X-SSEA-Key: ssea_<your_key>" \ -H "Content-Type: application/json" \ -d '{ "event_type": "quiz_submitted", "user_id": "moodle_user_42", "course_id": "moodle_course_7", "module_id": "moodle_cm_13", "timestamp": "2026-04-13T14:30:00Z", "metadata": { "attempt_number": 2, "score_percent": 84.5, "time_taken_seconds": 1200 } }'await fetch('https://ingest.softsysanalytics.com/v1/events', { method: 'POST', headers: { 'X-SSEA-Key': process.env.SSEA_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ event_type: 'quiz_submitted', user_id: 'moodle_user_42', course_id: 'moodle_course_7', timestamp: new Date().toISOString(), metadata: { score_percent: 84.5 }, }),});import os, requests
requests.post( 'https://ingest.softsysanalytics.com/v1/events', headers={ 'X-SSEA-Key': os.environ['SSEA_API_KEY'], 'Content-Type': 'application/json', }, json={ 'event_type': 'quiz_submitted', 'user_id': 'moodle_user_42', 'course_id': 'moodle_course_7', 'timestamp': '2026-04-13T14:30:00Z', 'metadata': {'score_percent': 84.5}, },)Success response — 202 Accepted
{ "ok": true, "data": { "queued": 1, "event_id": "<deterministic_hash>" }}The 202 code indicates the event was accepted and queued, but not yet processed. The dashboard may take a few minutes to reflect newly queued events.
POST /v1/events/batch — Send a batch
Section titled “POST /v1/events/batch — Send a batch”Sends up to 500 events in a single request. Recommended for high-volume integrators or initial sync flows.
curl -X POST https://ingest.softsysanalytics.com/v1/events/batch \ -H "X-SSEA-Key: ssea_<your_key>" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "event_type": "course_viewed", "user_id": "u1", "course_id": "c1", "timestamp": "2026-04-13T10:00:00Z" }, { "event_type": "resource_viewed", "user_id": "u1", "course_id": "c1", "module_id": "m1", "timestamp": "2026-04-13T10:01:00Z" } ] }'Response — 202 Accepted
{ "ok": true, "data": { "queued": 2, "skipped": 0 }}queued: events accepted and queued.skipped: events discarded by validation (wrong format, unsupported types). Does not raise an error if at least one event is accepted.
POST /v1/sync — Moodle entity sync
Section titled “POST /v1/sync — Moodle entity sync”Used by the Moodle plugin to migrate historical data (users, courses, enrollments, activities, grades) to the SSEA backend. Not for event telemetry — use /v1/events for that.
This endpoint is intended for the Moodle plugin and its payload format is coupled to the plugin’s flow. If you build your own integrator, you likely want to send normal events via /v1/events.
GET /health — Health check
Section titled “GET /health — Health check”Does not require authentication. Useful for external monitoring and connectivity tests from the Moodle server.
curl https://ingest.softsysanalytics.com/healthResponse — 200 OK
{ "ok": true, "service": "ssea-ingest"}Event payload format
Section titled “Event payload format”The payload follows a common core with optional fields per event type. See Event types for the full catalog.
{ "event_type": "string (one of the 15 supported types)", "user_id": "string (required — Moodle user ID)", "course_id": "string (optional — Moodle course ID)", "module_id": "string (optional — course module ID)", "timestamp": "string (ISO 8601 UTC, required)", "context": { "object_id": "string (optional)", "context_level": "number (optional)" }, "metadata": "object (optional — event-specific fields)", "ip": "string (optional — hashed server-side before persisting)"}Notes:
event_type: must be one from the catalog. Unknown types are discarded.timestamp: when the event happened at the source (not when you send it). ISO 8601 UTC format withZ(2026-04-13T14:30:00Z).ip: if you send it, the ingest worker computesSHA-256(ip + salt)and discards the raw value before queuing. If you prefer not to send IP, the Moodle plugin has ano_ipoption.metadata: event-specific fields enriched by the plugin (e.g.score_percentforquiz_submitted). See catalog.
Response codes
Section titled “Response codes”| HTTP | Meaning | Client action |
|---|---|---|
202 Accepted | Event(s) accepted and queued. | Continue normally. |
400 Bad Request | Invalid or malformed JSON. | Review the request body. Do not retry without fixing. |
401 Unauthorized | Missing, invalid, or revoked API key. | Check the X-SSEA-Key header. If keys were valid before, contact support to confirm status. |
402 Payment Required | Daily plan quota exceeded. | Wait for reset (UTC midnight) or upgrade plan. See Plans & limits. |
422 Unprocessable Entity | Validation failed (e.g. required event_type missing, batch >500 events). | Fix the payload. Do not retry without fixing. |
429 Too Many Requests | Rate limit exceeded (see Retry-After header). | Wait the indicated time and retry with exponential backoff. |
503 Service Unavailable | Worker misconfigured (missing critical secret). | Very rare — contact support. |
Error format
Section titled “Error format”All error responses follow the format:
{ "ok": false, "error": "Human-readable message", "code": "CODE_CONSTANT"}Common error codes:
code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or expired API key. |
INVALID_JSON | 400 | Malformed JSON. |
VALIDATION_ERROR | 422 | Required field missing or wrong format. |
BATCH_TOO_LARGE | 422 | Batch with more than 500 events. |
PLAN_LIMIT_EXCEEDED | 402 | Daily plan quota exhausted. |
RATE_LIMITED | 429 | Too many requests in the current window. |
Rate limiting
Section titled “Rate limiting”- Default: 1,000 requests per minute, per tenant.
- Enterprise plans: up to 10,000 req/60s, contract-negotiated.
429responses include theRetry-After: <seconds>header.
The recommendation for clients is to implement exponential backoff: retry after 1s, 2s, 4s, 8s… with a reasonable cap (e.g. 5 retries). The Moodle plugin does this automatically.
Idempotency and deduplication
Section titled “Idempotency and deduplication”- Each event produces a deterministic ID based on a hash of its key fields.
- If you resend the same event (for example, after a network timeout followed by retry), the processing worker detects the duplicate and does not count it twice.
- This means you can safely retry
202requests: you do not duplicate data.